Compare commits
262 Commits
v1.1.4+for
...
v1.1.4+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5fab4a555 | ||
|
|
abe28179ec | ||
|
|
60d4e4d396 | ||
|
|
435e73d718 | ||
|
|
17dc0850d5 | ||
|
|
9667a32e44 | ||
|
|
4e6ba84bb3 | ||
|
|
8714b24388 | ||
|
|
495db142d7 | ||
|
|
ac5d11159f | ||
|
|
d1479f142b | ||
|
|
ee6ec631e8 | ||
|
|
dee21222a7 | ||
|
|
cd8123ca34 | ||
|
|
ceb08ea78d | ||
|
|
b79ba71228 | ||
|
|
2903874dbc | ||
|
|
cac5b554e2 | ||
|
|
c4adbc8e45 | ||
|
|
bb01077c3b | ||
|
|
b370fcda6d | ||
|
|
d364ebbb2f | ||
|
|
5b28468efd | ||
|
|
6fd58c9682 | ||
|
|
b580743619 | ||
|
|
4a9cb9f2dc | ||
|
|
202a5f9581 | ||
|
|
145f55817f | ||
|
|
79025c2f36 | ||
|
|
2515a8d381 | ||
|
|
ede7ece25a | ||
|
|
2db39f8c66 | ||
|
|
5f0382456f | ||
|
|
63b1b58c4e | ||
|
|
06f2f67f0c | ||
|
|
05c33be3f4 | ||
|
|
e274b7e6d5 | ||
|
|
0806d0c5ea | ||
|
|
5c67dd0188 | ||
|
|
b4358f51cb | ||
|
|
622c6d503d | ||
|
|
b190480d77 | ||
|
|
9a085beea8 | ||
|
|
1a42a77e24 | ||
|
|
e35794ef7d | ||
|
|
1f9611fc3e | ||
|
|
563afd487c | ||
|
|
e10faeefc4 | ||
|
|
65dbbb3d61 | ||
|
|
fa69868ca1 | ||
|
|
9c18de7b90 | ||
|
|
61bd19f6ff | ||
|
|
ba0689aef7 | ||
|
|
ad54e6bb4b | ||
|
|
f15fcb43da | ||
|
|
66208f5694 | ||
|
|
68863f28eb | ||
|
|
7feaf093e2 | ||
|
|
4ab9e25fec | ||
|
|
e14dfda2fd | ||
|
|
c9aae828e2 | ||
|
|
f346c0af26 | ||
|
|
f2557b7815 | ||
|
|
a2726f5b61 | ||
|
|
834ec1575d | ||
|
|
a30f5bdee8 | ||
|
|
4cef005286 | ||
|
|
58a05681fe | ||
|
|
2589faf499 | ||
|
|
a5bdf34289 | ||
|
|
09fdd7f492 | ||
|
|
519d8b887d | ||
|
|
a2f2263bf7 | ||
|
|
5b73b10b34 | ||
|
|
b7a4364a28 | ||
|
|
3f075aff7b | ||
|
|
f4c33a5970 | ||
|
|
809af0ec18 | ||
|
|
4ee640e072 | ||
|
|
1cbf310555 | ||
|
|
f1fdc8aa43 | ||
|
|
d696daece3 | ||
|
|
967bb09282 | ||
|
|
136d910b3b | ||
|
|
51eb48a455 | ||
|
|
6ee8afcf96 | ||
|
|
a59f2d4609 | ||
|
|
b75d871837 | ||
|
|
c72f93b990 | ||
|
|
586d337ead | ||
|
|
d84e10a22e | ||
|
|
351ec89207 | ||
|
|
7db7bf0220 | ||
|
|
a9764c4f46 | ||
|
|
a430b6a280 | ||
|
|
6a01124d13 | ||
|
|
2843e445e2 | ||
|
|
5c947d14b2 | ||
|
|
590adba3e3 | ||
|
|
efee249173 | ||
|
|
6d2ed27364 | ||
|
|
55716d742f | ||
|
|
e4555da735 | ||
|
|
8b4b99bec7 | ||
|
|
5de4b19969 | ||
|
|
a9460f401e | ||
|
|
012cca550e | ||
|
|
0c743db412 | ||
|
|
b819ee7d6d | ||
|
|
e7e3a249b5 | ||
|
|
980c580b55 | ||
|
|
e23c530e74 | ||
|
|
a64caccca2 | ||
|
|
829bcafcf2 | ||
|
|
e2a935c647 | ||
|
|
2e7afdb49e | ||
|
|
cdc965e026 | ||
|
|
dd4faa005e | ||
|
|
726ec7159c | ||
|
|
e74256ef6f | ||
|
|
a18718ca81 | ||
|
|
5a9bc0e269 | ||
|
|
2d39c62ff0 | ||
|
|
0da4f79413 | ||
|
|
2bdef776a2 | ||
|
|
1819d6f042 | ||
|
|
2f6a707847 | ||
|
|
4aaf017824 | ||
|
|
fb05ed48d0 | ||
|
|
49203ae539 | ||
|
|
d17660d516 | ||
|
|
513ce34671 | ||
|
|
44ce48009b | ||
|
|
a57ad67308 | ||
|
|
e63d04cea9 | ||
|
|
cf48cb6f75 | ||
|
|
542e53cf6a | ||
|
|
bab1d40038 | ||
|
|
2f4a8247e8 | ||
|
|
f0b9006c55 | ||
|
|
4bc14ef797 | ||
|
|
47d2cee3f1 | ||
|
|
088f53f5a9 | ||
|
|
fee660bf6c | ||
|
|
e97ecb89a9 | ||
|
|
1ab6a4532b | ||
|
|
b43ddd0d8b | ||
|
|
d0c4c2d594 | ||
|
|
e0ae079ea0 | ||
|
|
dfddbd15a9 | ||
|
|
29242c45a1 | ||
|
|
7ff0e59f4d | ||
|
|
2d0fe57a47 | ||
|
|
ba2b87749b | ||
|
|
0806af1261 | ||
|
|
09e92f3a18 | ||
|
|
eb5d0bb795 | ||
|
|
a8afba4067 | ||
|
|
7d66141c37 | ||
|
|
b6f3ea2eec | ||
|
|
2570445133 | ||
|
|
acbd22cf22 | ||
|
|
ef251b040a | ||
|
|
6c5bb69ba9 | ||
|
|
18f605e5c5 | ||
|
|
7599406449 | ||
|
|
670e4c8538 | ||
|
|
30458b115c | ||
|
|
da8933ec58 | ||
|
|
dc8ac51c83 | ||
|
|
c4747fdc72 | ||
|
|
58ba748ade | ||
|
|
c0d51ad58a | ||
|
|
a0da73f76f | ||
|
|
34b8888c8f | ||
|
|
74ad40f67c | ||
|
|
a7a29db8d5 | ||
|
|
86a938d31d | ||
|
|
d8d0830631 | ||
|
|
bba3b7476a | ||
|
|
4880c642fe | ||
|
|
76ad896461 | ||
|
|
b49159f9e0 | ||
|
|
cd8a80a6a1 | ||
|
|
3ce8aa7894 | ||
|
|
5f3645f716 | ||
|
|
5af96597d5 | ||
|
|
c2baf4e05f | ||
|
|
b356794da9 | ||
|
|
afe8f6cf6a | ||
|
|
ed0df82fe9 | ||
|
|
d3bc7a9790 | ||
|
|
9e7923bc50 | ||
|
|
851bf94c90 | ||
|
|
ae80b7d098 | ||
|
|
e6bb319d8b | ||
|
|
3101f1ad17 | ||
|
|
4416dfcae3 | ||
|
|
924b974b4b | ||
|
|
5d1dc97ac3 | ||
|
|
5cce8ca72c | ||
|
|
f2a0680af0 | ||
|
|
6203ded864 | ||
|
|
17f1eb88e4 | ||
|
|
9a52cc033a | ||
|
|
633c0f870d | ||
|
|
f9fe7819f9 | ||
|
|
f3d13545e7 | ||
|
|
f6b77777b5 | ||
|
|
340990fbd9 | ||
|
|
a7687f8e35 | ||
|
|
52aa4a5289 | ||
|
|
268accea14 | ||
|
|
101cde4d84 | ||
|
|
8863446f6a | ||
|
|
28a0824f6b | ||
|
|
4b16262a1a | ||
|
|
b1f9d0516d | ||
|
|
10e7cbf022 | ||
|
|
531b8ead04 | ||
|
|
4b2c94ab52 | ||
|
|
5b21747d5d | ||
|
|
a98becf2f4 | ||
|
|
9fda48cff0 | ||
|
|
54f9eace67 | ||
|
|
0e6f3df212 | ||
|
|
a8c3f1555e | ||
|
|
cd797a637b | ||
|
|
53b2eb59d3 | ||
|
|
09e2224596 | ||
|
|
5999aad21b | ||
|
|
874ce07c3e | ||
|
|
1787d08718 | ||
|
|
9a12be88da | ||
|
|
8f6bb74e61 | ||
|
|
e4c9eb089a | ||
|
|
0e635aec23 | ||
|
|
dc90c09cea | ||
|
|
06cb335a0a | ||
|
|
e67bd2972a | ||
|
|
5a681d3557 | ||
|
|
4200486aeb | ||
|
|
62411a563f | ||
|
|
2cabe94ba0 | ||
|
|
4a6baae97a | ||
|
|
bb12a66781 | ||
|
|
de5929d8d2 | ||
|
|
d7699ef079 | ||
|
|
3ab04ebca8 | ||
|
|
efa9f524f9 | ||
|
|
be0c7777b7 | ||
|
|
92652f6fbd | ||
|
|
4ee781dfd5 | ||
|
|
bf8ac4bc69 | ||
|
|
a8e7840c04 | ||
|
|
c91ebda1ff | ||
|
|
e7dc5030d5 | ||
|
|
4cf55e23ba | ||
|
|
8159af0b58 | ||
|
|
78d2aa96d7 | ||
|
|
5d056d5bea | ||
|
|
f500cc7ebf |
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Screenshots and screen recordings**
|
||||||
|
If applicable, add screenshots (and screen recordings, if possible) to help explain your problem.
|
||||||
|
|
||||||
|
**Version**
|
||||||
|
Megalodon version: [e.g. v1.1.4+fork.#]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
- Does this issue also occur with the respective upstream release? (Please test using the respective `upstream-xxxxxx.apk` provided in [Releases](https://github.com/sk22/megalodon/releases)) No / Yes (`mastodon#…`)
|
||||||
|
|
||||||
|
> In this case, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead. If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue!
|
||||||
|
|
||||||
|
**Crash log**
|
||||||
|
If you know your way around Android development tools, please consider attaching a crash log, if possible.
|
||||||
20
.github/ISSUE_TEMPLATE/feature-ui-request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature-ui-request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature/UI request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: feature
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
If applicable: a clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: It's something else…
|
||||||
|
about: Issues that can't be categorized as feature requests or bug reports
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
74
README.md
74
README.md
@@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
# Megalodon
|
# Megalodon
|
||||||
|
|
||||||
> 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, bookmarks and an image description viewer.
|
[](https://translate.codeberg.org/engage/megalodon/)
|
||||||
|
|
||||||
|
[](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk)
|
||||||
|
|
||||||
**Warning! [The latest version's integrated updater is broken](https://github.com/sk22/megalodon/issues/106) – I'll publish a fixed version ASAP! If you're not updating through Izzy's F-Droid repository (more sources to come, hopefully!), you'll have to download the upcoming release manually. Sorry about that!**
|
<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 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.
|
||||||
|
|
||||||
[](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,7 +28,7 @@ The Mastodon documentation has some more information about [Unlisted posting](ht
|
|||||||
|
|
||||||
### **Federated timeline**
|
### **Federated timeline**
|
||||||
|
|
||||||
**This allows you to chronologically see all Public posts from people on all other Fediverse instances your home instance is connected to.**
|
**This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.**
|
||||||
|
|
||||||
Despite being one of the main features of federated social media, the Federated timeline wasn’t included in the official Mastodon app – supposedly, because this conflicts with Google’s safety requirements for apps on the Play Store.
|
Despite being one of the main features of federated social media, the Federated timeline wasn’t included in the official Mastodon app – supposedly, because this conflicts with Google’s safety requirements for apps on the Play Store.
|
||||||
|
|
||||||
@@ -41,24 +46,32 @@ This is important to **ensure the content you’re sharing is as accessible as p
|
|||||||
|
|
||||||
On the Fediverse, it’s quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
|
On the Fediverse, it’s quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
|
||||||
|
|
||||||
### **Bookmarks**
|
|
||||||
|
|
||||||
**They allow for quickly saving posts and viewing them through the Bookmarks button on the top right of your profile.**
|
|
||||||
|
|
||||||
To bookmark a post, press the button between the Favorite and Share buttons on the bottom of the post. Bookmarks are saved privately, so the post authors won’t know you saved their post – the list of bookmarked posts is only visible to you.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
**Press the download button above 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.**
|
### From app stores
|
||||||
|
|
||||||
To install this app on your Android device, download the [latest release from GitHub](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk) and open it. You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/sk22/megalodon/releases) page.
|
* **[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)
|
||||||
|
|
||||||
|
Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first:
|
||||||
|
|
||||||
|
`https://apt.izzysoft.de/fdroid/repo`
|
||||||
|
|
||||||
|
* **[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)
|
||||||
|
|
||||||
|
* **[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)
|
||||||
|
|
||||||
|
### Directly from GitHub
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
[](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk)
|
||||||
|
|
||||||
|
You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/sk22/megalodon/releases) page.
|
||||||
|
|
||||||
Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)’s automatic update checker. Megalodon will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
|
Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)’s automatic update checker. Megalodon will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
|
||||||
|
|
||||||
### Other sources
|
|
||||||
|
|
||||||
* **[Izzy's F-Droid repository](https://apt.izzysoft.de/fdroid/repo)**: https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
@@ -66,8 +79,6 @@ Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastod
|
|||||||
|
|
||||||
All downloads can be found on the [Releases](https://github.com/sk22/megalodon/releases) page.
|
All downloads can be found on the [Releases](https://github.com/sk22/megalodon/releases) page.
|
||||||
|
|
||||||
**Warning! [The latest version's integrated updater is broken](https://github.com/sk22/megalodon/issues/106) – I'll publish a fixed version ASAP! If you're not updating through Izzy's F-Droid repository (more sources to come, hopefully!), you'll have to download the upcoming release manually. Sorry about that!**
|
|
||||||
|
|
||||||
**`megalodon.apk`**
|
**`megalodon.apk`**
|
||||||
|
|
||||||
Variant with an integrated updater. If you download Megalodon from here (and not from an app store), just download the regular `megalodon.apk`.
|
Variant with an integrated updater. If you download Megalodon from here (and not from an app store), just download the regular `megalodon.apk`.
|
||||||
@@ -80,6 +91,19 @@ This is an **unmodified version** of the official [Mastodon for Android](https:/
|
|||||||
|
|
||||||
Variant without the integrated updater. This is the variant to be published to F-Droid.org where an integrated updater is not necessary. -->
|
Variant without the integrated updater. This is the variant to be published to F-Droid.org where an integrated updater is not necessary. -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
### Translation
|
||||||
|
|
||||||
|
As with the source code, the translation is sourced from the official project, which you can contribute to on the official “**Mastodon for Android**” Crowdin project: https://crowdin.com/project/mastodon-for-android
|
||||||
|
|
||||||
|
There's also a handful of custom strings exclusive to this projects that would need to be translated. You can help translate **Megalodon** on Weblate: https://translate.codeberg.org/projects/megalodon/
|
||||||
|
|
||||||
|
[](https://translate.codeberg.org/engage/megalodon/)
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +130,11 @@ Variant without the integrated updater. This is the variant to be published to F
|
|||||||
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
|
* [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)
|
* [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/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
|
||||||
* [Long-click to copy username from profile](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/copy-username)
|
* [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)
|
||||||
|
|
||||||
|
|
||||||
### Behavior
|
### Behavior
|
||||||
@@ -118,6 +146,12 @@ Variant without the integrated updater. This is the variant to be published to F
|
|||||||
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
|
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
|
||||||
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
|
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
|
||||||
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
|
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
|
||||||
|
* [No ellipsis for long poll answers](https://github.com/mastodon/mastodon-android/commit/c9aae828e2518adccdc092e41f8d1f0489636271)
|
||||||
|
* [Show poll vote button for multiple and single answer polls](https://github.com/mastodon/mastodon-android/commit/e14dfda2fdf32f0fa3043504ac5831683a87559a)
|
||||||
|
* [Show own vote after voting](https://github.com/mastodon/mastodon-android/commit/4ab9e25fec4fd9c10b7a8ddd1be522b3cc12cf28) ([Closes issue](https://github.com/mastodon/mastodon-android/commit/4ab9e25fec4fd9c10b7a8ddd1be522b3cc12cf28))
|
||||||
|
* [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)
|
||||||
|
|
||||||
|
|
||||||
### Visual
|
### Visual
|
||||||
@@ -125,6 +159,8 @@ Variant without the integrated updater. This is the variant to be published to F
|
|||||||
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
|
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
|
||||||
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
|
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
|
||||||
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)
|
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)
|
||||||
|
* [Custom color themes](https://github.com/sk22/megalodon/pull/124) by [@LucasGGamerM](https://github.com/LucasGGamerM)
|
||||||
|
* [Custom "megalodon" text logo](https://github.com/sk22/megalodon/commit/563afd487ca5c608cfbb00fa3909d3c27384acc0) by [@LucasGGamerM](https://github.com/LucasGGamerM)
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ buildscript {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||||
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
git rev-parse --short --verify upstream/master
|
|
||||||
BIN
img/f-droid-badge.png
Normal file
BIN
img/f-droid-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,16 +1,16 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk 33
|
compileSdk 33
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
archivesBaseName = "megalodon"
|
||||||
applicationId "org.joinmastodon.android.sk"
|
applicationId "org.joinmastodon.android.sk"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 50
|
versionCode 58
|
||||||
versionName "1.1.4+fork.50"
|
versionName "1.1.4+fork.58"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
|
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
|
||||||
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
|
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
|
||||||
@@ -29,19 +29,15 @@ android {
|
|||||||
versionNameSuffix '-debug'
|
versionNameSuffix '-debug'
|
||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix '.debug'
|
||||||
}
|
}
|
||||||
appcenterPrivateBeta{
|
|
||||||
initWith release
|
|
||||||
minifyEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
versionNameSuffix "-priv-beta"
|
|
||||||
}
|
|
||||||
appcenterPublicBeta{
|
|
||||||
initWith release
|
|
||||||
versionNameSuffix "-beta"
|
|
||||||
}
|
|
||||||
githubRelease{
|
githubRelease{
|
||||||
initWith release
|
initWith release
|
||||||
}
|
}
|
||||||
|
playRelease{
|
||||||
|
initWith release
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
|
versionNameSuffix '-play'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
@@ -49,12 +45,6 @@ android {
|
|||||||
coreLibraryDesugaringEnabled true
|
coreLibraryDesugaringEnabled true
|
||||||
}
|
}
|
||||||
sourceSets{
|
sourceSets{
|
||||||
appcenterPrivateBeta{
|
|
||||||
setRoot "src/appcenter"
|
|
||||||
}
|
|
||||||
appcenterPublicBeta{
|
|
||||||
setRoot "src/appcenter"
|
|
||||||
}
|
|
||||||
githubRelease{
|
githubRelease{
|
||||||
setRoot "src/github"
|
setRoot "src/github"
|
||||||
}
|
}
|
||||||
@@ -86,12 +76,6 @@ dependencies {
|
|||||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||||
|
|
||||||
def appCenterSdkVersion = "4.4.2"
|
|
||||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
|
||||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
|
||||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
|
||||||
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
|
androidTestImplementation 'androidx.test:core:1.4.1-alpha05'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
|
||||||
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
|
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
|
||||||
|
|||||||
6
mastodon/proguard-rules.pro
vendored
6
mastodon/proguard-rules.pro
vendored
@@ -40,12 +40,6 @@
|
|||||||
@com.squareup.otto.Subscribe <methods>;
|
@com.squareup.otto.Subscribe <methods>;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class com.microsoft.appcenter.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
|
||||||
|
|
||||||
-keepattributes LineNumberTable
|
-keepattributes LineNumberTable
|
||||||
|
|
||||||
# Parceler library
|
# Parceler library
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package org.joinmastodon.android;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.microsoft.appcenter.AppCenter;
|
|
||||||
import com.microsoft.appcenter.crashes.Crashes;
|
|
||||||
import com.microsoft.appcenter.distribute.Distribute;
|
|
||||||
import com.microsoft.appcenter.distribute.UpdateTrack;
|
|
||||||
|
|
||||||
public class AppCenterWrapper{
|
|
||||||
private static final String TAG="AppCenterWrapper";
|
|
||||||
|
|
||||||
public static void init(Application app){
|
|
||||||
if(AppCenter.isConfigured())
|
|
||||||
return;
|
|
||||||
Log.i(TAG, "initializing AppCenter SDK, build type is "+BuildConfig.BUILD_TYPE);
|
|
||||||
|
|
||||||
if(BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta"))
|
|
||||||
Distribute.setUpdateTrack(UpdateTrack.PRIVATE);
|
|
||||||
AppCenter.start(app, BuildConfig.appCenterKey, Distribute.class, Crashes.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,14 +8,13 @@
|
|||||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
|
||||||
|
|
||||||
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
|
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MastodonApp"
|
android:name=".MastodonApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/sk_app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE"/>
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<data android:scheme="mastodon-android-auth" android:host="callback"/>
|
<data android:scheme="megalodon-android-auth" android:host="callback"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">
|
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||||||
getWindow().setBackgroundDrawable(null);
|
getWindow().setBackgroundDrawable(null);
|
||||||
|
|
||||||
Intent intent=getIntent();
|
Intent intent=getIntent();
|
||||||
String text=intent.getStringExtra(Intent.EXTRA_TEXT);
|
StringBuilder builder=new StringBuilder();
|
||||||
|
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) builder.append(intent.getStringExtra(Intent.EXTRA_SUBJECT)).append("\n");
|
||||||
|
if (intent.hasExtra(Intent.EXTRA_TEXT)) builder.append(intent.getStringExtra(Intent.EXTRA_TEXT)).append("\n");
|
||||||
|
String text=builder.toString();
|
||||||
List<Uri> mediaUris;
|
List<Uri> mediaUris;
|
||||||
if(Intent.ACTION_SEND.equals(intent.getAction())){
|
if(Intent.ACTION_SEND.equals(intent.getAction())){
|
||||||
Uri singleUri=intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
Uri singleUri=intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ package org.joinmastodon.android;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.LocaleList;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class GlobalUserPreferences{
|
public class GlobalUserPreferences{
|
||||||
public static boolean playGifs;
|
public static boolean playGifs;
|
||||||
@@ -10,10 +18,28 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean showReplies;
|
public static boolean showReplies;
|
||||||
public static boolean showBoosts;
|
public static boolean showBoosts;
|
||||||
public static boolean loadNewPosts;
|
public static boolean loadNewPosts;
|
||||||
|
public static boolean showFederatedTimeline;
|
||||||
public static boolean showInteractionCounts;
|
public static boolean showInteractionCounts;
|
||||||
public static boolean alwaysExpandContentWarnings;
|
public static boolean alwaysExpandContentWarnings;
|
||||||
public static boolean disableMarquee;
|
public static boolean disableMarquee;
|
||||||
|
public static boolean voteButtonForSingleChoice;
|
||||||
public static ThemePreference theme;
|
public static ThemePreference theme;
|
||||||
|
public static ColorPreference color;
|
||||||
|
public static List<String> recentLanguages;
|
||||||
|
|
||||||
|
private static String defaultRecentLanguages;
|
||||||
|
|
||||||
|
static {
|
||||||
|
List<Locale> systemLocales = new ArrayList<>();;
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
|
systemLocales.add(Resources.getSystem().getConfiguration().locale);
|
||||||
|
} else {
|
||||||
|
LocaleList localeList = Resources.getSystem().getConfiguration().getLocales();
|
||||||
|
for (int i = 0; i < localeList.size(); i++) systemLocales.add(localeList.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRecentLanguages = systemLocales.stream().map(Locale::getLanguage).collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
|
||||||
private static SharedPreferences getPrefs(){
|
private static SharedPreferences getPrefs(){
|
||||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||||
@@ -27,10 +53,14 @@ public class GlobalUserPreferences{
|
|||||||
showReplies=prefs.getBoolean("showReplies", true);
|
showReplies=prefs.getBoolean("showReplies", true);
|
||||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||||
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
||||||
|
showFederatedTimeline=prefs.getBoolean("showFederatedTimeline", !BuildConfig.BUILD_TYPE.equals("playRelease"));
|
||||||
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||||
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||||
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
||||||
|
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
|
||||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||||
|
color=ColorPreference.values()[prefs.getInt("color", 0)];
|
||||||
|
recentLanguages=Arrays.stream(prefs.getString("recentLanguages", defaultRecentLanguages).split(",")).filter(s->!s.isEmpty()).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void save(){
|
public static void save(){
|
||||||
@@ -40,17 +70,30 @@ public class GlobalUserPreferences{
|
|||||||
.putBoolean("showReplies", showReplies)
|
.putBoolean("showReplies", showReplies)
|
||||||
.putBoolean("showBoosts", showBoosts)
|
.putBoolean("showBoosts", showBoosts)
|
||||||
.putBoolean("loadNewPosts", loadNewPosts)
|
.putBoolean("loadNewPosts", loadNewPosts)
|
||||||
|
.putBoolean("showFederatedTimeline", showFederatedTimeline)
|
||||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||||
.putBoolean("showInteractionCounts", showInteractionCounts)
|
.putBoolean("showInteractionCounts", showInteractionCounts)
|
||||||
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
||||||
.putBoolean("disableMarquee", disableMarquee)
|
.putBoolean("disableMarquee", disableMarquee)
|
||||||
.putInt("theme", theme.ordinal())
|
.putInt("theme", theme.ordinal())
|
||||||
|
.putInt("color", color.ordinal())
|
||||||
|
.putString("recentLanguages", String.join(",", recentLanguages))
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ColorPreference{
|
||||||
|
PINK,
|
||||||
|
PURPLE,
|
||||||
|
GREEN,
|
||||||
|
BLUE,
|
||||||
|
BROWN,
|
||||||
|
YELLOW
|
||||||
|
}
|
||||||
|
|
||||||
public enum ThemePreference{
|
public enum ThemePreference{
|
||||||
AUTO,
|
AUTO,
|
||||||
LIGHT,
|
LIGHT,
|
||||||
DARK
|
DARK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package org.joinmastodon.android;
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Application;
|
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageInstaller;
|
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -24,8 +22,6 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
|||||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
|
|
||||||
@@ -70,12 +66,7 @@ public class MainActivity extends FragmentStackActivity{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(BuildConfig.BUILD_TYPE.startsWith("appcenter")){
|
if(GithubSelfUpdater.needSelfUpdating()){
|
||||||
// Call the appcenter SDK wrapper through reflection because it is only present in beta builds
|
|
||||||
try{
|
|
||||||
Class.forName("org.joinmastodon.android.AppCenterWrapper").getMethod("init", Application.class).invoke(null, getApplication());
|
|
||||||
}catch(ClassNotFoundException|NoSuchMethodException|IllegalAccessException|InvocationTargetException ignore){}
|
|
||||||
}else if(GithubSelfUpdater.needSelfUpdating()){
|
|
||||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import android.content.Context;
|
|||||||
|
|
||||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
|
|
||||||
import me.grishka.appkit.imageloader.ImageCache;
|
import me.grishka.appkit.imageloader.ImageCache;
|
||||||
import me.grishka.appkit.utils.NetworkUtils;
|
import me.grishka.appkit.utils.NetworkUtils;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.api;
|
package org.joinmastodon.android.api;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonIOException;
|
import com.google.gson.JsonIOException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -26,6 +27,9 @@ public class JsonObjectRequestBody extends RequestBody{
|
|||||||
public void writeTo(BufferedSink sink) throws IOException{
|
public void writeTo(BufferedSink sink) throws IOException{
|
||||||
try{
|
try{
|
||||||
OutputStreamWriter writer=new OutputStreamWriter(sink.outputStream(), StandardCharsets.UTF_8);
|
OutputStreamWriter writer=new OutputStreamWriter(sink.outputStream(), StandardCharsets.UTF_8);
|
||||||
|
if(obj instanceof JsonElement)
|
||||||
|
writer.write(obj.toString());
|
||||||
|
else
|
||||||
MastodonAPIController.gson.toJson(obj, writer);
|
MastodonAPIController.gson.toJson(obj, writer);
|
||||||
writer.flush();
|
writer.flush();
|
||||||
}catch(JsonIOException x){
|
}catch(JsonIOException x){
|
||||||
|
|||||||
@@ -365,6 +365,8 @@ public class PushSubscriptionManager{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void registerAllAccountsForPush(boolean forceReRegister){
|
private static void registerAllAccountsForPush(boolean forceReRegister){
|
||||||
|
if(!arePushNotificationsAvailable())
|
||||||
|
return;
|
||||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||||
if(session.pushSubscription==null || forceReRegister)
|
if(session.pushSubscription==null || forceReRegister)
|
||||||
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);
|
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);
|
||||||
|
|||||||
@@ -63,36 +63,6 @@ public class StatusInteractionController{
|
|||||||
E.post(new StatusCountersUpdatedEvent(status));
|
E.post(new StatusCountersUpdatedEvent(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBookmarked(Status status, boolean bookmarked){
|
|
||||||
if(!Looper.getMainLooper().isCurrentThread())
|
|
||||||
throw new IllegalStateException("Can only be called from main thread");
|
|
||||||
|
|
||||||
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
|
|
||||||
if(current!=null){
|
|
||||||
current.cancel();
|
|
||||||
}
|
|
||||||
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
|
|
||||||
.setCallback(new Callback<>(){
|
|
||||||
@Override
|
|
||||||
public void onSuccess(Status result){
|
|
||||||
runningBookmarkRequests.remove(status.id);
|
|
||||||
E.post(new StatusCountersUpdatedEvent(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(ErrorResponse error){
|
|
||||||
runningBookmarkRequests.remove(status.id);
|
|
||||||
error.showToast(MastodonApp.context);
|
|
||||||
status.bookmarked=!bookmarked;
|
|
||||||
E.post(new StatusCountersUpdatedEvent(status));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec(accountID);
|
|
||||||
runningBookmarkRequests.put(status.id, req);
|
|
||||||
status.bookmarked=bookmarked;
|
|
||||||
E.post(new StatusCountersUpdatedEvent(status));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReblogged(Status status, boolean reblogged){
|
public void setReblogged(Status status, boolean reblogged){
|
||||||
if(!Looper.getMainLooper().isCurrentThread())
|
if(!Looper.getMainLooper().isCurrentThread())
|
||||||
throw new IllegalStateException("Can only be called from main thread");
|
throw new IllegalStateException("Can only be called from main thread");
|
||||||
@@ -130,4 +100,34 @@ public class StatusInteractionController{
|
|||||||
status.reblogsCount--;
|
status.reblogsCount--;
|
||||||
E.post(new StatusCountersUpdatedEvent(status));
|
E.post(new StatusCountersUpdatedEvent(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBookmarked(Status status, boolean bookmarked){
|
||||||
|
if(!Looper.getMainLooper().isCurrentThread())
|
||||||
|
throw new IllegalStateException("Can only be called from main thread");
|
||||||
|
|
||||||
|
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
|
||||||
|
if(current!=null){
|
||||||
|
current.cancel();
|
||||||
|
}
|
||||||
|
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Status result){
|
||||||
|
runningBookmarkRequests.remove(status.id);
|
||||||
|
E.post(new StatusCountersUpdatedEvent(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
runningBookmarkRequests.remove(status.id);
|
||||||
|
error.showToast(MastodonApp.context);
|
||||||
|
status.bookmarked=!bookmarked;
|
||||||
|
E.post(new StatusCountersUpdatedEvent(status));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
runningBookmarkRequests.put(status.id, req);
|
||||||
|
status.bookmarked=bookmarked;
|
||||||
|
E.post(new StatusCountersUpdatedEvent(status));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,21 @@ public class IsoInstantTypeAdapter extends TypeAdapter<Instant>{
|
|||||||
in.nextNull();
|
in.nextNull();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
String nextString;
|
||||||
try {
|
try {
|
||||||
return DateTimeFormatter.ISO_INSTANT.parse(in.nextString(), Instant::from);
|
nextString = in.nextString();
|
||||||
}catch(DateTimeParseException x){
|
}catch(Exception e){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
return DateTimeFormatter.ISO_INSTANT.parse(nextString, Instant::from);
|
||||||
|
}catch(DateTimeParseException x){}
|
||||||
|
|
||||||
|
try{
|
||||||
|
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(nextString, Instant::from);
|
||||||
|
}catch(DateTimeParseException x){}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.joinmastodon.android.api.gson;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
|
||||||
|
public class JsonArrayBuilder{
|
||||||
|
private JsonArray arr=new JsonArray();
|
||||||
|
|
||||||
|
public JsonArrayBuilder add(JsonElement el){
|
||||||
|
arr.add(el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonArrayBuilder add(String el){
|
||||||
|
arr.add(el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonArrayBuilder add(Number el){
|
||||||
|
arr.add(el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonArrayBuilder add(boolean el){
|
||||||
|
arr.add(el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonArrayBuilder add(JsonObjectBuilder el){
|
||||||
|
arr.add(el.build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonArrayBuilder add(JsonArrayBuilder el){
|
||||||
|
arr.add(el.build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonArray build(){
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.joinmastodon.android.api.gson;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
public class JsonObjectBuilder{
|
||||||
|
private JsonObject obj=new JsonObject();
|
||||||
|
|
||||||
|
public JsonObjectBuilder add(String key, JsonElement el){
|
||||||
|
obj.add(key, el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObjectBuilder add(String key, String el){
|
||||||
|
obj.addProperty(key, el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObjectBuilder add(String key, Number el){
|
||||||
|
obj.addProperty(key, el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObjectBuilder add(String key, boolean el){
|
||||||
|
obj.addProperty(key, el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObjectBuilder add(String key, JsonObjectBuilder el){
|
||||||
|
obj.add(key, el.build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObjectBuilder add(String key, JsonArrayBuilder el){
|
||||||
|
obj.add(key, el.build());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject build(){
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package org.joinmastodon.android.api.requests.accounts;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
|
|
||||||
private String maxId;
|
|
||||||
|
|
||||||
public GetBookmarks(String maxID, String minID, int limit){
|
|
||||||
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
|
|
||||||
if(maxID!=null)
|
|
||||||
addQueryParameter("max_id", maxID);
|
|
||||||
if(minID!=null)
|
|
||||||
addQueryParameter("min_id", minID);
|
|
||||||
if(limit>0)
|
|
||||||
addQueryParameter("limit", ""+limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
|
|
||||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
|
||||||
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
|
|
||||||
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
|
|
||||||
String link=httpResponse.header("link");
|
|
||||||
// parsing link header by hand; using a library would be cleaner
|
|
||||||
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
|
||||||
if(link==null) return;
|
|
||||||
String maxIdEq="max_id=";
|
|
||||||
for(String s : link.split(",")) {
|
|
||||||
if(s.contains("rel=\"next\"")) {
|
|
||||||
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
|
||||||
int end=s.indexOf('>');
|
|
||||||
if(start<0 || start>end) return;
|
|
||||||
this.maxId=s.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMaxId() {
|
|
||||||
return maxId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package org.joinmastodon.android.api.requests.accounts;
|
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class GetFavourites extends MastodonAPIRequest<List<Status>>{
|
|
||||||
private String maxId;
|
|
||||||
|
|
||||||
public GetFavourites(String maxID, String minID, int limit){
|
|
||||||
super(HttpMethod.GET, "/favourites", new TypeToken<>(){});
|
|
||||||
if(maxID!=null)
|
|
||||||
addQueryParameter("max_id", maxID);
|
|
||||||
if(minID!=null)
|
|
||||||
addQueryParameter("min_id", minID);
|
|
||||||
if(limit>0)
|
|
||||||
addQueryParameter("limit", ""+limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
|
|
||||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
|
||||||
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
|
|
||||||
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
|
|
||||||
String link=httpResponse.header("link");
|
|
||||||
// parsing link header by hand; using a library would be cleaner
|
|
||||||
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
|
||||||
if(link==null) return;
|
|
||||||
String maxIdEq="max_id=";
|
|
||||||
for(String s : link.split(",")) {
|
|
||||||
if(s.contains("rel=\"next\"")) {
|
|
||||||
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
|
||||||
int end=s.indexOf('>');
|
|
||||||
if(start<0 || start>end) return;
|
|
||||||
this.maxId=s.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMaxId() {
|
|
||||||
return maxId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,49 +2,15 @@ package org.joinmastodon.android.api.requests.accounts;
|
|||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.FollowSuggestion;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
public class GetFollowRequests extends HeaderPaginationRequest<Account>{
|
||||||
import java.util.List;
|
public GetFollowRequests(String maxID, int limit){
|
||||||
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class GetFollowRequests extends MastodonAPIRequest<List<Account>>{
|
|
||||||
private String maxId;
|
|
||||||
|
|
||||||
public GetFollowRequests(String maxID, String minID, int limit){
|
|
||||||
super(HttpMethod.GET, "/follow_requests", new TypeToken<>(){});
|
super(HttpMethod.GET, "/follow_requests", new TypeToken<>(){});
|
||||||
if(maxID!=null)
|
if(maxID!=null)
|
||||||
addQueryParameter("max_id", maxID);
|
addQueryParameter("max_id", maxID);
|
||||||
if(minID!=null)
|
|
||||||
addQueryParameter("min_id", minID);
|
|
||||||
if(limit>0)
|
if(limit>0)
|
||||||
addQueryParameter("limit", ""+limit);
|
addQueryParameter("limit", ""+limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void validateAndPostprocessResponse(List<Account> respObj, Response httpResponse) throws IOException {
|
|
||||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
|
||||||
// <https://mastodon.social/api/v1/follow_requests?max_id=268962>; rel="next",
|
|
||||||
// <https://mastodon.social/api/v1/follow_requests?min_id=268981>; rel="prev"
|
|
||||||
String link=httpResponse.header("link");
|
|
||||||
// parsing link header by hand; using a library would be cleaner
|
|
||||||
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
|
||||||
if(link==null) return;
|
|
||||||
String maxIdEq="max_id=";
|
|
||||||
for(String s : link.split(",")) {
|
|
||||||
if(s.contains("rel=\"next\"")) {
|
|
||||||
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
|
||||||
int end=s.indexOf('>');
|
|
||||||
if(start<0 || start>end) return;
|
|
||||||
this.maxId=s.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMaxId() {
|
|
||||||
return maxId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,15 @@ public class GetInstance extends MastodonAPIRequest<Instance>{
|
|||||||
public GetInstance(){
|
public GetInstance(){
|
||||||
super(HttpMethod.GET, "/instance", Instance.class);
|
super(HttpMethod.GET, "/instance", Instance.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class V2 extends MastodonAPIRequest<Instance.V2>{
|
||||||
|
public V2(){
|
||||||
|
super(HttpMethod.GET, "/instance", Instance.V2.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getPathPrefix() {
|
||||||
|
return "/api/v2";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.markers;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||||
|
import org.joinmastodon.android.model.Marker;
|
||||||
|
|
||||||
|
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||||
|
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
|
||||||
|
super(HttpMethod.POST, "/markers", Response.class);
|
||||||
|
JsonObjectBuilder builder=new JsonObjectBuilder();
|
||||||
|
if(lastSeenHomePostID!=null)
|
||||||
|
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
|
||||||
|
if(lastSeenNotificationID!=null)
|
||||||
|
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
|
||||||
|
setRequestBody(builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response{
|
||||||
|
public Marker home, notifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Status;
|
||||||
|
|
||||||
|
public class GetBookmarkedStatuses extends HeaderPaginationRequest<Status>{
|
||||||
|
public GetBookmarkedStatuses(String maxID, int limit){
|
||||||
|
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Status;
|
||||||
|
|
||||||
|
public class GetFavoritedStatuses extends HeaderPaginationRequest<Status>{
|
||||||
|
public GetFavoritedStatuses(String maxID, int limit){
|
||||||
|
super(HttpMethod.GET, "/favourites", new TypeToken<>(){});
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
if(limit>0)
|
||||||
|
addQueryParameter("limit", limit+"");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.statuses;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.TranslatedStatus;
|
||||||
|
|
||||||
|
public class TranslateStatus extends MastodonAPIRequest<TranslatedStatus> {
|
||||||
|
public TranslateStatus(String id) {
|
||||||
|
super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class);
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
|
|||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Application;
|
import org.joinmastodon.android.model.Application;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ public class AccountSession{
|
|||||||
public long filtersLastUpdated;
|
public long filtersLastUpdated;
|
||||||
public List<Filter> wordFilters=new ArrayList<>();
|
public List<Filter> wordFilters=new ArrayList<>();
|
||||||
public String pushAccountID;
|
public String pushAccountID;
|
||||||
|
public Preferences preferences;
|
||||||
private transient MastodonAPIController apiController;
|
private transient MastodonAPIController apiController;
|
||||||
private transient StatusInteractionController statusInteractionController;
|
private transient StatusInteractionController statusInteractionController;
|
||||||
private transient CacheController cacheController;
|
private transient CacheController cacheController;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.joinmastodon.android.MastodonApp;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
|
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
|
||||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||||
@@ -34,6 +35,7 @@ import org.joinmastodon.android.model.Emoji;
|
|||||||
import org.joinmastodon.android.model.EmojiCategory;
|
import org.joinmastodon.android.model.EmojiCategory;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -61,7 +63,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
|||||||
public class AccountSessionManager{
|
public class AccountSessionManager{
|
||||||
private static final String TAG="AccountSessionManager";
|
private static final String TAG="AccountSessionManager";
|
||||||
public static final String SCOPE="read write follow push";
|
public static final String SCOPE="read write follow push";
|
||||||
public static final String REDIRECT_URI="mastodon-android-auth://callback";
|
public static final String REDIRECT_URI="megalodon-android-auth://callback";
|
||||||
|
|
||||||
private static final AccountSessionManager instance=new AccountSessionManager();
|
private static final AccountSessionManager instance=new AccountSessionManager();
|
||||||
|
|
||||||
@@ -211,7 +213,7 @@ public class AccountSessionManager{
|
|||||||
.path("/oauth/authorize")
|
.path("/oauth/authorize")
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.appendQueryParameter("client_id", result.clientId)
|
.appendQueryParameter("client_id", result.clientId)
|
||||||
.appendQueryParameter("redirect_uri", "mastodon-android-auth://callback")
|
.appendQueryParameter("redirect_uri", "megalodon-android-auth://callback")
|
||||||
.appendQueryParameter("scope", SCOPE)
|
.appendQueryParameter("scope", SCOPE)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -248,12 +250,13 @@ public class AccountSessionManager{
|
|||||||
HashSet<String> domains=new HashSet<>();
|
HashSet<String> domains=new HashSet<>();
|
||||||
for(AccountSession session:sessions.values()){
|
for(AccountSession session:sessions.values()){
|
||||||
domains.add(session.domain.toLowerCase());
|
domains.add(session.domain.toLowerCase());
|
||||||
if(now-session.infoLastUpdated>24L*3600_000L){
|
// if(now-session.infoLastUpdated>24L*3600_000L){
|
||||||
|
updateSessionPreferences(session);
|
||||||
updateSessionLocalInfo(session);
|
updateSessionLocalInfo(session);
|
||||||
}
|
// }
|
||||||
if(now-session.filtersLastUpdated>3600_000L){
|
// if(now-session.filtersLastUpdated>3600_000L){
|
||||||
updateSessionWordFilters(session);
|
updateSessionWordFilters(session);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
if(loadedInstances){
|
if(loadedInstances){
|
||||||
maybeUpdateCustomEmojis(domains);
|
maybeUpdateCustomEmojis(domains);
|
||||||
@@ -263,10 +266,10 @@ public class AccountSessionManager{
|
|||||||
private void maybeUpdateCustomEmojis(Set<String> domains){
|
private void maybeUpdateCustomEmojis(Set<String> domains){
|
||||||
long now=System.currentTimeMillis();
|
long now=System.currentTimeMillis();
|
||||||
for(String domain:domains){
|
for(String domain:domains){
|
||||||
Long lastUpdated=instancesLastUpdated.get(domain);
|
// Long lastUpdated=instancesLastUpdated.get(domain);
|
||||||
if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
||||||
updateInstanceInfo(domain);
|
updateInstanceInfo(domain);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +291,18 @@ public class AccountSessionManager{
|
|||||||
.exec(session.getID());
|
.exec(session.getID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateSessionPreferences(AccountSession session){
|
||||||
|
new GetPreferences().setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Preferences preferences) {
|
||||||
|
session.preferences=preferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {}
|
||||||
|
}).exec(session.getID());
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSessionWordFilters(AccountSession session){
|
private void updateSessionWordFilters(AccountSession session){
|
||||||
new GetWordFilters()
|
new GetWordFilters()
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@@ -313,6 +328,11 @@ public class AccountSessionManager{
|
|||||||
public void onSuccess(Instance instance){
|
public void onSuccess(Instance instance){
|
||||||
instances.put(domain, instance);
|
instances.put(domain, instance);
|
||||||
updateInstanceEmojis(instance, domain);
|
updateInstanceEmojis(instance, domain);
|
||||||
|
try {
|
||||||
|
if (Integer.parseInt(instance.version.split("\\.")[0]) >= 4) {
|
||||||
|
updateInstanceInfoV2(domain);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -323,6 +343,19 @@ public class AccountSessionManager{
|
|||||||
.execNoAuth(domain);
|
.execNoAuth(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateInstanceInfoV2(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse errorResponse) {}
|
||||||
|
}).execNoAuth(domain);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateInstanceEmojis(Instance instance, String domain){
|
private void updateInstanceEmojis(Instance instance, String domain){
|
||||||
new GetCustomEmojis()
|
new GetCustomEmojis()
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@@ -398,6 +431,10 @@ public class AccountSessionManager{
|
|||||||
return instances.get(domain);
|
return instances.get(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Instance getInstanceInfoForAccount(String account) {
|
||||||
|
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
|
||||||
|
}
|
||||||
|
|
||||||
public void updateAccountInfo(String id, Account account){
|
public void updateAccountInfo(String id, Account account){
|
||||||
AccountSession session=getAccount(id);
|
AccountSession session=getAccount(id);
|
||||||
session.self=account;
|
session.self=account;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class RemoveAccountPostsEvent{
|
||||||
|
public final String accountID;
|
||||||
|
public final String postsByAccountID;
|
||||||
|
public final boolean isUnfollow;
|
||||||
|
|
||||||
|
public RemoveAccountPostsEvent(String accountID, String postsByAccountID, boolean isUnfollow){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.postsByAccountID=postsByAccountID;
|
||||||
|
this.isUnfollow=isUnfollow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import org.joinmastodon.android.model.Status;
|
|||||||
public class StatusCountersUpdatedEvent{
|
public class StatusCountersUpdatedEvent{
|
||||||
public String id;
|
public String id;
|
||||||
public long favorites, reblogs, replies;
|
public long favorites, reblogs, replies;
|
||||||
public boolean favorited, reblogged, pinned;
|
public boolean favorited, reblogged, bookmarked, pinned;
|
||||||
|
|
||||||
public StatusCountersUpdatedEvent(Status s){
|
public StatusCountersUpdatedEvent(Status s){
|
||||||
id=s.id;
|
id=s.id;
|
||||||
@@ -14,6 +14,7 @@ public class StatusCountersUpdatedEvent{
|
|||||||
replies=s.repliesCount;
|
replies=s.repliesCount;
|
||||||
favorited=s.favourited;
|
favorited=s.favourited;
|
||||||
reblogged=s.reblogged;
|
reblogged=s.reblogged;
|
||||||
|
bookmarked=s.bookmarked;
|
||||||
pinned=s.pinned;
|
pinned=s.pinned;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package org.joinmastodon.android.events;
|
|||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
public class StatusCreatedEvent{
|
public class StatusCreatedEvent{
|
||||||
public Status status;
|
public final Status status;
|
||||||
|
public final String accountID;
|
||||||
|
|
||||||
public StatusCreatedEvent(Status status){
|
public StatusCreatedEvent(Status status, String accountID){
|
||||||
this.status=status;
|
this.status=status;
|
||||||
|
this.accountID=accountID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.View;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
@@ -109,4 +110,9 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
displayItems.subList(index, lastIndex).clear();
|
displayItems.subList(index, lastIndex).clear();
|
||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,10 +400,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
|
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
|
||||||
Poll poll=holder.getItem().poll;
|
Poll poll=holder.getItem().poll;
|
||||||
Poll.Option option=holder.getItem().option;
|
Poll.Option option=holder.getItem().option;
|
||||||
if(poll.multiple){
|
if(poll.multiple || GlobalUserPreferences.voteButtonForSingleChoice){
|
||||||
if(poll.selectedOptions==null)
|
if(poll.selectedOptions==null)
|
||||||
poll.selectedOptions=new ArrayList<>();
|
poll.selectedOptions=new ArrayList<>();
|
||||||
if(poll.selectedOptions.contains(option)){
|
boolean optionContained=poll.selectedOptions.contains(option);
|
||||||
|
if(!poll.multiple) poll.selectedOptions.clear();
|
||||||
|
if(optionContained){
|
||||||
poll.selectedOptions.remove(option);
|
poll.selectedOptions.remove(option);
|
||||||
holder.itemView.setSelected(false);
|
holder.itemView.setSelected(false);
|
||||||
}else{
|
}else{
|
||||||
@@ -412,6 +414,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
for(int i=0;i<list.getChildCount();i++){
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
||||||
|
if(!poll.multiple && vh instanceof PollOptionStatusDisplayItem.Holder item){
|
||||||
|
if (item != holder) item.itemView.setSelected(false);
|
||||||
|
}
|
||||||
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
|
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
|
||||||
if(footer.getItemID().equals(holder.getItemID())){
|
if(footer.getItemID().equals(holder.getItemID())){
|
||||||
footer.rebind();
|
footer.rebind();
|
||||||
@@ -444,7 +449,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
error.showToast(getActivity());
|
error.showToast(getActivity());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress(getActivity(), R.string.loading, false)
|
.wrapProgress(getActivity(), R.string.loading, true)
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||||
|
private String nextMaxID;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Activity activity){
|
||||||
|
super.onAttach(activity);
|
||||||
|
setTitle(R.string.bookmarks);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetBookmarkedStatuses(offset==0 ? null : nextMaxID, count)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Status> result){
|
||||||
|
if(result.nextPageUri!=null)
|
||||||
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
|
else
|
||||||
|
nextMaxID=null;
|
||||||
|
onDataLoaded(result, nextMaxID!=null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
import org.joinmastodon.android.model.Account;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
|
||||||
|
|
||||||
public class BookmarksListFragment extends StatusListFragment{
|
|
||||||
|
|
||||||
private String accountID;
|
|
||||||
private Account self;
|
|
||||||
private String lastMaxId=null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState){
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
accountID=getArguments().getString("account");
|
|
||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
|
||||||
self=session.self;
|
|
||||||
setTitle(R.string.bookmarks);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onShown(){
|
|
||||||
super.onShown();
|
|
||||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doLoadData(int offset, int count) {
|
|
||||||
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
|
|
||||||
currentRequest=b.setCallback(new SimpleCallback<>(this){
|
|
||||||
@Override
|
|
||||||
public void onSuccess(List<Status> result){
|
|
||||||
onDataLoaded(result, b.getMaxId()!=null);
|
|
||||||
lastMaxId=b.getMaxId();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec(accountID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,13 +28,14 @@ import android.text.Layout;
|
|||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.text.format.DateUtils;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.SubMenu;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewOutlineProvider;
|
import android.view.ViewOutlineProvider;
|
||||||
@@ -42,6 +43,7 @@ import android.view.WindowManager;
|
|||||||
import android.view.animation.LinearInterpolator;
|
import android.view.animation.LinearInterpolator;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
import android.widget.CheckBox;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@@ -52,10 +54,10 @@ import android.widget.ProgressBar;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.twitter.twittertext.Regex;
|
|
||||||
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
@@ -102,9 +104,10 @@ import org.parceler.Parcels;
|
|||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -132,22 +135,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
// from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift
|
// from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift
|
||||||
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
|
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
|
||||||
private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
|
private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
|
||||||
|
private static final List<Locale> allIsoLanguages;
|
||||||
|
|
||||||
private static final String VALID_URL_PATTERN_STRING =
|
|
||||||
"(" + // $1 total match
|
|
||||||
"(" + Regex.URL_VALID_PRECEDING_CHARS + ")" + // $2 Preceding character
|
|
||||||
"(" + // $3 URL
|
|
||||||
"(https?://)" + // $4 Protocol (optional)
|
|
||||||
"(" + Regex.URL_VALID_DOMAIN + ")" + // $5 Domain(s)
|
|
||||||
"(?::(" + Regex.URL_VALID_PORT_NUMBER + "))?" + // $6 Port number (optional)
|
|
||||||
"(/" +
|
|
||||||
Regex.URL_VALID_PATH + "*+" +
|
|
||||||
")?" + // $7 URL Path and anchor
|
|
||||||
"(\\?" + Regex.URL_VALID_URL_QUERY_CHARS + "*" + // $8 Query String
|
|
||||||
Regex.URL_VALID_URL_QUERY_ENDING_CHARS + ")?" +
|
|
||||||
")" +
|
|
||||||
")";
|
|
||||||
private static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
|
|
||||||
@SuppressLint("NewApi") // this class actually exists on 6.0
|
@SuppressLint("NewApi") // this class actually exists on 6.0
|
||||||
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
|
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
|
||||||
|
|
||||||
@@ -162,7 +151,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
private String accountID;
|
private String accountID;
|
||||||
private int charCount, charLimit, trimmedCharCount;
|
private int charCount, charLimit, trimmedCharCount;
|
||||||
|
|
||||||
private Button publishButton;
|
private Button publishButton, languageButton;
|
||||||
|
private PopupMenu languagePopup;
|
||||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
|
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
|
||||||
private ImageView sensitiveIcon;
|
private ImageView sensitiveIcon;
|
||||||
private ComposeMediaLayout attachmentsView;
|
private ComposeMediaLayout attachmentsView;
|
||||||
@@ -171,6 +161,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
private View pollWrap;
|
private View pollWrap;
|
||||||
private View addPollOptionBtn;
|
private View addPollOptionBtn;
|
||||||
private View sensitiveItem;
|
private View sensitiveItem;
|
||||||
|
private View pollAllowMultipleItem;
|
||||||
|
private CheckBox pollAllowMultipleCheckbox;
|
||||||
private TextView pollDurationView;
|
private TextView pollDurationView;
|
||||||
|
|
||||||
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
||||||
@@ -205,6 +197,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
private boolean ignoreSelectionChanges=false;
|
private boolean ignoreSelectionChanges=false;
|
||||||
private Runnable updateUploadEtaRunnable;
|
private Runnable updateUploadEtaRunnable;
|
||||||
|
|
||||||
|
private String language;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Locale[] locales = Locale.getAvailableLocales();
|
||||||
|
List<Locale> allLocales = new ArrayList<>();
|
||||||
|
List<String> addedLanguages = new ArrayList<>();
|
||||||
|
for (Locale locale : locales) {
|
||||||
|
String lang = locale.getLanguage();
|
||||||
|
if (!addedLanguages.contains(lang)) {
|
||||||
|
allLocales.add(locale);
|
||||||
|
addedLanguages.add(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(allLocales, Comparator.comparing(l -> l.getDisplayLanguage(Locale.getDefault())));
|
||||||
|
allIsoLanguages = allLocales;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -234,8 +244,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
charLimit=instance.configuration.statuses.maxCharacters;
|
charLimit=instance.configuration.statuses.maxCharacters;
|
||||||
else
|
else
|
||||||
charLimit=500;
|
charLimit=500;
|
||||||
|
|
||||||
loadDefaultStatusVisibility(savedInstanceState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -315,6 +323,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
pollOptionsView=view.findViewById(R.id.poll_options);
|
pollOptionsView=view.findViewById(R.id.poll_options);
|
||||||
pollWrap=view.findViewById(R.id.poll_wrap);
|
pollWrap=view.findViewById(R.id.poll_wrap);
|
||||||
addPollOptionBtn=view.findViewById(R.id.add_poll_option);
|
addPollOptionBtn=view.findViewById(R.id.add_poll_option);
|
||||||
|
pollAllowMultipleItem=view.findViewById(R.id.poll_allow_multiple);
|
||||||
|
pollAllowMultipleCheckbox=view.findViewById(R.id.poll_allow_multiple_checkbox);
|
||||||
|
pollAllowMultipleItem.setOnClickListener(v->this.togglePollAllowMultiple());
|
||||||
|
|
||||||
addPollOptionBtn.setOnClickListener(v->{
|
addPollOptionBtn.setOnClickListener(v->{
|
||||||
createDraftPollOption().edit.requestFocus();
|
createDraftPollOption().edit.requestFocus();
|
||||||
@@ -329,6 +340,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
pollBtn.setSelected(true);
|
pollBtn.setSelected(true);
|
||||||
mediaBtn.setEnabled(false);
|
mediaBtn.setEnabled(false);
|
||||||
pollWrap.setVisibility(View.VISIBLE);
|
pollWrap.setVisibility(View.VISIBLE);
|
||||||
|
updatePollAllowMultiple(savedInstanceState.getBoolean("pollAllowMultiple", false));
|
||||||
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){
|
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){
|
||||||
DraftPollOption opt=createDraftPollOption();
|
DraftPollOption opt=createDraftPollOption();
|
||||||
opt.edit.setText(oldText);
|
opt.edit.setText(oldText);
|
||||||
@@ -339,6 +351,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
pollBtn.setSelected(true);
|
pollBtn.setSelected(true);
|
||||||
mediaBtn.setEnabled(false);
|
mediaBtn.setEnabled(false);
|
||||||
pollWrap.setVisibility(View.VISIBLE);
|
pollWrap.setVisibility(View.VISIBLE);
|
||||||
|
updatePollAllowMultiple(editingStatus.poll.multiple);
|
||||||
for(Poll.Option eopt:editingStatus.poll.options){
|
for(Poll.Option eopt:editingStatus.poll.options){
|
||||||
DraftPollOption opt=createDraftPollOption();
|
DraftPollOption opt=createDraftPollOption();
|
||||||
opt.edit.setText(eopt.title);
|
opt.edit.setText(eopt.title);
|
||||||
@@ -385,6 +398,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
statusVisibility=editingStatus.visibility;
|
statusVisibility=editingStatus.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadDefaultStatusVisibility(savedInstanceState);
|
||||||
updateVisibilityIcon();
|
updateVisibilityIcon();
|
||||||
|
|
||||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||||
@@ -409,9 +423,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
outState.putStringArrayList("pollOptions", opts);
|
outState.putStringArrayList("pollOptions", opts);
|
||||||
outState.putInt("pollDuration", pollDuration);
|
outState.putInt("pollDuration", pollDuration);
|
||||||
outState.putString("pollDurationStr", pollDurationStr);
|
outState.putString("pollDurationStr", pollDurationStr);
|
||||||
|
outState.putBoolean("pollAllowMultiple", pollAllowMultipleItem.isSelected());
|
||||||
}
|
}
|
||||||
outState.putBoolean("sensitive", sensitive);
|
outState.putBoolean("sensitive", sensitive);
|
||||||
outState.putBoolean("hasSpoiler", hasSpoiler);
|
outState.putBoolean("hasSpoiler", hasSpoiler);
|
||||||
|
outState.putString("language", language);
|
||||||
if(!attachments.isEmpty()){
|
if(!attachments.isEmpty()){
|
||||||
ArrayList<Parcelable> serializedAttachments=new ArrayList<>(attachments.size());
|
ArrayList<Parcelable> serializedAttachments=new ArrayList<>(attachments.size());
|
||||||
for(DraftMediaAttachment att:attachments){
|
for(DraftMediaAttachment att:attachments){
|
||||||
@@ -505,7 +521,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
|
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
|
||||||
int visibilityNameRes = switch (statusVisibility) {
|
int visibilityNameRes = switch (statusVisibility) {
|
||||||
case PUBLIC -> R.string.visibility_public;
|
case PUBLIC -> R.string.visibility_public;
|
||||||
case UNLISTED -> R.string.visibility_unlisted;
|
case UNLISTED -> R.string.sk_visibility_unlisted;
|
||||||
case PRIVATE -> R.string.visibility_followers_only;
|
case PRIVATE -> R.string.visibility_followers_only;
|
||||||
case DIRECT -> R.string.visibility_private;
|
case DIRECT -> R.string.visibility_private;
|
||||||
};
|
};
|
||||||
@@ -521,6 +537,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
replyArrow.setBounds(0, 0, V.dp(20), V.dp(20));
|
replyArrow.setBounds(0, 0, V.dp(20), V.dp(20));
|
||||||
replyText.setCompoundDrawables(replyArrow, null, visibilityIcon, null);
|
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);
|
||||||
|
});
|
||||||
ArrayList<String> mentions=new ArrayList<>();
|
ArrayList<String> mentions=new ArrayList<>();
|
||||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||||
if(!replyTo.account.id.equals(ownID))
|
if(!replyTo.account.id.equals(ownID))
|
||||||
@@ -544,6 +567,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
spoilerEdit.setText(replyTo.spoilerText);
|
spoilerEdit.setText(replyTo.spoilerText);
|
||||||
spoilerBtn.setSelected(true);
|
spoilerBtn.setSelected(true);
|
||||||
}
|
}
|
||||||
|
if (replyTo.language != null && !replyTo.language.isEmpty()) updateLanguage(replyTo.language);
|
||||||
}
|
}
|
||||||
}else if (editingStatus==null || editingStatus.inReplyToId==null){
|
}else if (editingStatus==null || editingStatus.inReplyToId==null){
|
||||||
// TODO: remove workaround after https://github.com/mastodon/mastodon-android/issues/341 gets fixed
|
// TODO: remove workaround after https://github.com/mastodon/mastodon-android/issues/341 gets fixed
|
||||||
@@ -556,6 +580,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
ignoreSelectionChanges=true;
|
ignoreSelectionChanges=true;
|
||||||
mainEditText.setSelection(mainEditText.length());
|
mainEditText.setSelection(mainEditText.length());
|
||||||
ignoreSelectionChanges=false;
|
ignoreSelectionChanges=false;
|
||||||
|
updateLanguage(editingStatus.language);
|
||||||
if(!editingStatus.mediaAttachments.isEmpty()){
|
if(!editingStatus.mediaAttachments.isEmpty()){
|
||||||
attachmentsView.setVisibility(View.VISIBLE);
|
attachmentsView.setVisibility(View.VISIBLE);
|
||||||
for(Attachment att:editingStatus.mediaAttachments){
|
for(Attachment att:editingStatus.mediaAttachments){
|
||||||
@@ -618,6 +643,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
sendError.setVisibility(View.GONE);
|
sendError.setVisibility(View.GONE);
|
||||||
sendProgress.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.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.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||||
wrap.setClipToPadding(false);
|
wrap.setClipToPadding(false);
|
||||||
@@ -627,6 +656,59 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
updatePublishButtonState();
|
updatePublishButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateLanguage(String lang) {
|
||||||
|
updateLanguage(new Locale(lang));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLanguage(Locale loc) {
|
||||||
|
language = loc.getLanguage();
|
||||||
|
languageButton.setText(loc.getDisplayLanguage(loc));
|
||||||
|
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, loc.getDisplayLanguage(Locale.getDefault())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private Button buildLanguageSelector() {
|
||||||
|
languageButton=new Button(getActivity());
|
||||||
|
TypedValue typedValue = new TypedValue();
|
||||||
|
getActivity().getTheme().resolveAttribute(android.R.attr.textColorSecondary, typedValue, true);
|
||||||
|
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));
|
||||||
|
|
||||||
|
languagePopup=new PopupMenu(getActivity(), languageButton);
|
||||||
|
languageButton.setOnTouchListener(languagePopup.getDragToOpenListener());
|
||||||
|
languageButton.setOnClickListener(v->languagePopup.show());
|
||||||
|
|
||||||
|
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
|
||||||
|
String defaultLang = prefs != null ? prefs.postingDefaultLanguage : null;
|
||||||
|
if (defaultLang != null) updateLanguage(defaultLang);
|
||||||
|
else updateLanguage(Locale.getDefault());
|
||||||
|
|
||||||
|
Menu languageMenu = languagePopup.getMenu();
|
||||||
|
|
||||||
|
for (String recentLanguage : GlobalUserPreferences.recentLanguages) {
|
||||||
|
Locale loc = new Locale(recentLanguage);
|
||||||
|
languageMenu.add(0, allIsoLanguages.indexOf(loc), Menu.NONE, loc.getDisplayLanguage(Locale.getDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
|
SubMenu allLanguagesMenu = languageMenu.addSubMenu(R.string.sk_all_languages);
|
||||||
|
for (int i = 0; i < allIsoLanguages.size(); i++) {
|
||||||
|
Locale loc = allIsoLanguages.get(i);
|
||||||
|
allLanguagesMenu.add(0, i, Menu.NONE, loc.getDisplayLanguage(Locale.getDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
|
languagePopup.setOnMenuItemClickListener(i->{
|
||||||
|
if (i.hasSubMenu()) return false;
|
||||||
|
updateLanguage(allIsoLanguages.get(i.getItemId()));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return languageButton;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item){
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
return true;
|
return true;
|
||||||
@@ -644,7 +726,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
|
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
|
||||||
MENTION_PATTERN.matcher(
|
MENTION_PATTERN.matcher(
|
||||||
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
HtmlParser.URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
||||||
).replaceAll("$1@$3")
|
).replaceAll("$1@$3")
|
||||||
).replaceAll("x");
|
).replaceAll("x");
|
||||||
charCount=0;
|
charCount=0;
|
||||||
@@ -700,6 +782,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
req.status=text;
|
req.status=text;
|
||||||
req.visibility=statusVisibility;
|
req.visibility=statusVisibility;
|
||||||
req.sensitive=sensitive;
|
req.sensitive=sensitive;
|
||||||
|
req.language=language;
|
||||||
if(!attachments.isEmpty()){
|
if(!attachments.isEmpty()){
|
||||||
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@@ -709,6 +792,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
if(!pollOptions.isEmpty()){
|
if(!pollOptions.isEmpty()){
|
||||||
req.poll=new CreateStatus.Request.Poll();
|
req.poll=new CreateStatus.Request.Poll();
|
||||||
req.poll.expiresIn=pollDuration;
|
req.poll.expiresIn=pollDuration;
|
||||||
|
req.poll.multiple=pollAllowMultipleItem.isSelected();
|
||||||
for(DraftPollOption opt:pollOptions)
|
for(DraftPollOption opt:pollOptions)
|
||||||
req.poll.options.add(opt.edit.getText().toString());
|
req.poll.options.add(opt.edit.getText().toString());
|
||||||
}
|
}
|
||||||
@@ -738,7 +822,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
wm.removeView(sendingOverlay);
|
wm.removeView(sendingOverlay);
|
||||||
sendingOverlay=null;
|
sendingOverlay=null;
|
||||||
if(editingStatus==null){
|
if(editingStatus==null){
|
||||||
E.post(new StatusCreatedEvent(result));
|
E.post(new StatusCreatedEvent(result, accountID));
|
||||||
if(replyTo!=null){
|
if(replyTo!=null){
|
||||||
replyTo.repliesCount++;
|
replyTo.repliesCount++;
|
||||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||||
@@ -776,6 +860,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
.setCallback(resCallback)
|
.setCallback(resCallback)
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> recentLanguages = new ArrayList<>(GlobalUserPreferences.recentLanguages);
|
||||||
|
recentLanguages.remove(language);
|
||||||
|
recentLanguages.add(0, language);
|
||||||
|
GlobalUserPreferences.recentLanguages = recentLanguages.stream().limit(4).collect(Collectors.toList());
|
||||||
|
GlobalUserPreferences.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasDraft(){
|
private boolean hasDraft(){
|
||||||
@@ -1219,6 +1309,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
option.view=LayoutInflater.from(getActivity()).inflate(R.layout.compose_poll_option, pollOptionsView, false);
|
option.view=LayoutInflater.from(getActivity()).inflate(R.layout.compose_poll_option, pollOptionsView, false);
|
||||||
option.edit=option.view.findViewById(R.id.edit);
|
option.edit=option.view.findViewById(R.id.edit);
|
||||||
option.dragger=option.view.findViewById(R.id.dragger_thingy);
|
option.dragger=option.view.findViewById(R.id.dragger_thingy);
|
||||||
|
ImageView icon = option.view.findViewById(R.id.icon);
|
||||||
|
icon.setImageDrawable(getContext().getDrawable(pollAllowMultipleItem.isSelected() ?
|
||||||
|
R.drawable.ic_poll_checkbox_regular_selector :
|
||||||
|
R.drawable.ic_poll_option_button
|
||||||
|
));
|
||||||
|
|
||||||
option.dragger.setOnLongClickListener(v->{
|
option.dragger.setOnLongClickListener(v->{
|
||||||
pollOptionsView.startDragging(option.view);
|
pollOptionsView.startDragging(option.view);
|
||||||
@@ -1354,13 +1449,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||||
}
|
}
|
||||||
|
|
||||||
new GetPreferences()
|
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
|
||||||
.setCallback(new Callback<>(){
|
if (prefs != null) {
|
||||||
@Override
|
|
||||||
public void onSuccess(Preferences result){
|
|
||||||
// Only override the reply visibility if our preference is more private
|
// Only override the reply visibility if our preference is more private
|
||||||
if (result.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
|
if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
|
||||||
statusVisibility = switch (result.postingDefaultVisibility) {
|
statusVisibility = switch (prefs.postingDefaultVisibility) {
|
||||||
case PUBLIC -> StatusPrivacy.PUBLIC;
|
case PUBLIC -> StatusPrivacy.PUBLIC;
|
||||||
case UNLISTED -> StatusPrivacy.UNLISTED;
|
case UNLISTED -> StatusPrivacy.UNLISTED;
|
||||||
case PRIVATE -> StatusPrivacy.PRIVATE;
|
case PRIVATE -> StatusPrivacy.PRIVATE;
|
||||||
@@ -1372,16 +1465,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
if(savedInstanceState !=null){
|
if(savedInstanceState !=null){
|
||||||
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVisibilityIcon ();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(ErrorResponse error){
|
|
||||||
Log.w(TAG, "Unable to get user preferences to set default post privacy");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec(accountID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateVisibilityIcon(){
|
private void updateVisibilityIcon(){
|
||||||
@@ -1396,6 +1480,27 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void togglePollAllowMultiple() {
|
||||||
|
updatePollAllowMultiple(!pollAllowMultipleItem.isSelected());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePollAllowMultiple(boolean multiple){
|
||||||
|
pollAllowMultipleItem.setSelected(multiple);
|
||||||
|
pollAllowMultipleCheckbox.setChecked(multiple);
|
||||||
|
ImageView btn = addPollOptionBtn.findViewById(R.id.add_poll_option_icon);
|
||||||
|
btn.setImageDrawable(getContext().getDrawable(multiple ?
|
||||||
|
R.drawable.ic_fluent_add_square_24_regular :
|
||||||
|
R.drawable.ic_fluent_add_circle_24_regular
|
||||||
|
));
|
||||||
|
for (DraftPollOption opt:pollOptions) {
|
||||||
|
ImageView icon = opt.view.findViewById(R.id.icon);
|
||||||
|
icon.setImageDrawable(getContext().getDrawable(multiple ?
|
||||||
|
R.drawable.ic_poll_checkbox_regular_selector :
|
||||||
|
R.drawable.ic_poll_option_button
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSelectionChanged(int start, int end){
|
public void onSelectionChanged(int start, int end){
|
||||||
if(ignoreSelectionChanges)
|
if(ignoreSelectionChanges)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class FavoritedStatusListFragment extends StatusListFragment{
|
||||||
|
private String nextMaxID;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Activity activity){
|
||||||
|
super.onAttach(activity);
|
||||||
|
setTitle(R.string.your_favorites);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetFavoritedStatuses(offset==0 ? null : nextMaxID, count)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<Status> result){
|
||||||
|
if(result.nextPageUri!=null)
|
||||||
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
|
else
|
||||||
|
nextMaxID=null;
|
||||||
|
onDataLoaded(result, nextMaxID!=null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import org.joinmastodon.android.R;
|
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetFavourites;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
import org.joinmastodon.android.model.Status;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
|
||||||
|
|
||||||
public class FavoritesListFragment extends StatusListFragment{
|
|
||||||
|
|
||||||
private String accountID;
|
|
||||||
private String lastMaxId=null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState){
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
accountID=getArguments().getString("account");
|
|
||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
|
||||||
setTitle(R.string.favorited_posts);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onShown(){
|
|
||||||
super.onShown();
|
|
||||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doLoadData(int offset, int count) {
|
|
||||||
GetFavourites b=new GetFavourites(offset>0 ? lastMaxId : null, null, count);
|
|
||||||
currentRequest=b.setCallback(new SimpleCallback<>(this){
|
|
||||||
@Override
|
|
||||||
public void onSuccess(List<Status> result){
|
|
||||||
onDataLoaded(result, b.getMaxId()!=null);
|
|
||||||
lastMaxId=b.getMaxId();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.exec(accountID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
@@ -50,7 +51,7 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowReque
|
|||||||
private String accountID;
|
private String accountID;
|
||||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||||
private GetAccountRelationships relationshipsRequest;
|
private GetAccountRelationships relationshipsRequest;
|
||||||
private String lastMaxId=null;
|
private String nextMaxID;
|
||||||
|
|
||||||
public FollowRequestsListFragment(){
|
public FollowRequestsListFragment(){
|
||||||
super(20);
|
super(20);
|
||||||
@@ -66,7 +67,7 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowReque
|
|||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity) {
|
public void onAttach(Activity activity) {
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
setTitle(R.string.follow_requests);
|
setTitle(R.string.sk_follow_requests);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -75,10 +76,14 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowReque
|
|||||||
relationshipsRequest.cancel();
|
relationshipsRequest.cancel();
|
||||||
relationshipsRequest=null;
|
relationshipsRequest=null;
|
||||||
}
|
}
|
||||||
currentRequest=new GetFollowRequests(offset>0 ? lastMaxId : null, null, count)
|
currentRequest=new GetFollowRequests(offset==0 ? null : nextMaxID, count)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Account> result){
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
|
if(result.nextPageUri!=null)
|
||||||
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
|
else
|
||||||
|
nextMaxID=null;
|
||||||
onDataLoaded(result.stream().map(AccountWrapper::new).collect(Collectors.toList()), false);
|
onDataLoaded(result.stream().map(AccountWrapper::new).collect(Collectors.toList()), false);
|
||||||
loadRelationships();
|
loadRelationships();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
setTitle(R.string.app_name);
|
setTitle(R.string.sk_app_name);
|
||||||
|
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||||
setRetainInstance(true);
|
setRetainInstance(true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter;
|
|||||||
import android.animation.AnimatorSet;
|
import android.animation.AnimatorSet;
|
||||||
import android.animation.ObjectAnimator;
|
import android.animation.ObjectAnimator;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -17,6 +18,7 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@ import me.grishka.appkit.utils.V;
|
|||||||
|
|
||||||
public class HomeTimelineFragment extends StatusListFragment{
|
public class HomeTimelineFragment extends StatusListFragment{
|
||||||
private ImageButton fab;
|
private ImageButton fab;
|
||||||
private TextView toolbarLogo;
|
private ImageView toolbarLogo;
|
||||||
private Button toolbarShowNewPostsBtn;
|
private Button toolbarShowNewPostsBtn;
|
||||||
private boolean newPostsBtnShown;
|
private boolean newPostsBtnShown;
|
||||||
private AnimatorSet currentNewPostsAnim;
|
private AnimatorSet currentNewPostsAnim;
|
||||||
@@ -315,9 +317,10 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateToolbarLogo(){
|
private void updateToolbarLogo(){
|
||||||
toolbarLogo =new TextView(getActivity());
|
toolbarLogo=new ImageView(getActivity());
|
||||||
toolbarLogo.setText(getString(R.string.app_name).toLowerCase(Locale.getDefault()));
|
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
|
||||||
toolbarLogo.setTextAppearance(R.style.app_title);
|
toolbarLogo.setImageResource(R.drawable.logo);
|
||||||
|
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||||
|
|
||||||
toolbarShowNewPostsBtn=new Button(getActivity());
|
toolbarShowNewPostsBtn=new Button(getActivity());
|
||||||
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
|
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
|
||||||
@@ -345,7 +348,9 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
FrameLayout logoWrap=new FrameLayout(getActivity());
|
FrameLayout logoWrap=new FrameLayout(getActivity());
|
||||||
logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
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));
|
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
|
||||||
|
|
||||||
Toolbar toolbar=getToolbar();
|
Toolbar toolbar=getToolbar();
|
||||||
@@ -432,4 +437,9 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||||
updateUpdateState(ev.state);
|
updateUpdateState(ev.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
|||||||
if(args.containsKey("profileAccount")){
|
if(args.containsKey("profileAccount")){
|
||||||
profileAccountId=args.getString("profileAccount");
|
profileAccountId=args.getString("profileAccount");
|
||||||
profileDisplayUsername=args.getString("profileDisplayUsername");
|
profileDisplayUsername=args.getString("profileDisplayUsername");
|
||||||
setTitle(getString(R.string.lists_with_user, profileDisplayUsername));
|
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
|
||||||
// setHasOptionsMenu(true);
|
// setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
||||||
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||||
@@ -29,8 +30,6 @@ import androidx.viewpager2.widget.ViewPager2;
|
|||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
@@ -167,9 +166,9 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void refreshFollowRequestsBadge() {
|
public void refreshFollowRequestsBadge() {
|
||||||
new GetFollowRequests(null, null, 1).setCallback(new Callback<>() {
|
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Account> accounts) {
|
public void onSuccess(HeaderPaginationList<Account> accounts) {
|
||||||
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
|
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import com.squareup.otto.Subscribe;
|
|||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.NotificationDeletedEvent;
|
import org.joinmastodon.android.events.NotificationDeletedEvent;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
@@ -27,6 +29,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
@@ -122,6 +125,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
loadRelationships(needRelationships);
|
loadRelationships(needRelationships);
|
||||||
maxID=result.maxID;
|
maxID=result.maxID;
|
||||||
|
|
||||||
|
if(offset==0 && !result.items.isEmpty()){
|
||||||
|
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -192,14 +199,23 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onNotificationDeleted(NotificationDeletedEvent ev) {
|
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
Notification notification = getNotificationByID(ev.id);
|
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||||
if(notification==null)
|
|
||||||
return;
|
return;
|
||||||
data.remove(notification);
|
List<Notification> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
||||||
|
.filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for(Notification n:toRemove){
|
||||||
|
removeNotification(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeNotification(Notification n){
|
||||||
|
data.remove(n);
|
||||||
|
preloadedData.remove(n);
|
||||||
int index=-1;
|
int index=-1;
|
||||||
for(int i=0;i<displayItems.size();i++){
|
for(int i=0;i<displayItems.size();i++){
|
||||||
if(ev.id.equals(displayItems.get(i).parentID)){
|
if(n.id.equals(displayItems.get(i).parentID)){
|
||||||
index=i;
|
index=i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -208,11 +224,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
return;
|
return;
|
||||||
int lastIndex;
|
int lastIndex;
|
||||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||||
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
displayItems.subList(index, lastIndex).clear();
|
displayItems.subList(index, lastIndex).clear();
|
||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
|||||||
}
|
}
|
||||||
|
|
||||||
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
|
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
|
||||||
private ShapeDrawable background=new ShapeDrawable();
|
protected ShapeDrawable background=new ShapeDrawable();
|
||||||
|
|
||||||
public BaseViewHolder(int layout){
|
public BaseViewHolder(int layout){
|
||||||
super(getActivity(), layout, list);
|
super(getActivity(), layout, list);
|
||||||
@@ -220,6 +220,20 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
|||||||
super.onBind(item);
|
super.onBind(item);
|
||||||
title.setText(item.parsedName);
|
title.setText(item.parsedName);
|
||||||
value.setText(item.parsedValue);
|
value.setText(item.parsedValue);
|
||||||
|
if(item.verifiedAt!=null){
|
||||||
|
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
|
||||||
|
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
|
||||||
|
value.setTextColor(textColor);
|
||||||
|
value.setLinkTextColor(textColor);
|
||||||
|
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
|
||||||
|
check.setTint(textColor);
|
||||||
|
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
|
||||||
|
}else{
|
||||||
|
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||||
|
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||||
|
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
|
||||||
|
value.setCompoundDrawables(null, null, null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import android.app.Activity;
|
|||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.Outline;
|
import android.graphics.Outline;
|
||||||
@@ -19,8 +18,6 @@ import android.graphics.drawable.Drawable;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.VibrationEffect;
|
|
||||||
import android.os.Vibrator;
|
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ImageSpan;
|
import android.text.style.ImageSpan;
|
||||||
@@ -252,7 +249,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
tab.setText(switch(position){
|
tab.setText(switch(position){
|
||||||
case 0 -> R.string.posts;
|
case 0 -> R.string.posts;
|
||||||
case 1 -> R.string.posts_and_replies;
|
case 1 -> R.string.posts_and_replies;
|
||||||
case 2 -> R.string.pinned_posts;
|
case 2 -> R.string.sk_pinned_posts;
|
||||||
case 3 -> R.string.media;
|
case 3 -> R.string.media;
|
||||||
case 4 -> R.string.profile_about;
|
case 4 -> R.string.profile_about;
|
||||||
default -> throw new IllegalStateException();
|
default -> throw new IllegalStateException();
|
||||||
@@ -286,6 +283,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
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();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return sizeWrapper;
|
return sizeWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,16 +462,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
// noinspection SetTextI18n
|
// noinspection SetTextI18n
|
||||||
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
||||||
}
|
}
|
||||||
username.setOnLongClickListener(l->{
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(CLIPBOARD_SERVICE);
|
|
||||||
Vibrator v = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
|
|
||||||
ClipData clip = ClipData.newPlainText("Username", '@'+account.acct+'@'+AccountSessionManager.getInstance().getAccount(accountID).domain);
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
Toast.makeText(getActivity(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) v.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE));
|
|
||||||
else v.vibrate(50);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||||
if(TextUtils.isEmpty(parsedBio)){
|
if(TextUtils.isEmpty(parsedBio)){
|
||||||
bio.setVisibility(View.GONE);
|
bio.setVisibility(View.GONE);
|
||||||
@@ -548,25 +547,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
}
|
}
|
||||||
if(relationship==null && !isOwnProfile)
|
if(relationship==null && !isOwnProfile)
|
||||||
return;
|
return;
|
||||||
inflater.inflate(R.menu.profile, menu);
|
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
|
||||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.lists_with_user, account.getDisplayUsername()));
|
if(isOwnProfile)
|
||||||
if(isOwnProfile){
|
|
||||||
for(int i=0;i<menu.size();i++){
|
|
||||||
MenuItem item=menu.getItem(i);
|
|
||||||
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks || item.getItemId()==R.id.manage_user_lists);
|
|
||||||
}
|
|
||||||
menu.findItem(R.id.favorites_list).setVisible(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_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.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.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||||
|
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
|
||||||
if(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()));
|
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);
|
||||||
}else {
|
}else {
|
||||||
menu.findItem(R.id.hide_boosts).setVisible(false);
|
menu.findItem(R.id.hide_boosts).setVisible(false);
|
||||||
menu.findItem(R.id.manage_user_lists).setVisible(false);
|
manageUserLists.setVisible(false);
|
||||||
}
|
}
|
||||||
if(!account.isLocal())
|
if(!account.isLocal())
|
||||||
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||||
@@ -582,16 +578,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
intent.setType("text/plain");
|
intent.setType("text/plain");
|
||||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||||
}else if(id==R.id.bookmarks) {
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
|
||||||
Nav.go(getActivity(), BookmarksListFragment.class, args);
|
|
||||||
}else if(id==R.id.favorites_list) {
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
|
||||||
Nav.go(getActivity(), FavoritesListFragment.class, args);
|
|
||||||
}else if(id==R.id.mute){
|
}else if(id==R.id.mute){
|
||||||
confirmToggleMuted();
|
confirmToggleMuted();
|
||||||
}else if(id==R.id.block){
|
}else if(id==R.id.block){
|
||||||
@@ -623,6 +609,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
})
|
})
|
||||||
.wrapProgress(getActivity(), R.string.loading, false)
|
.wrapProgress(getActivity(), R.string.loading, false)
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
|
}else if(id==R.id.bookmarks){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Nav.go(getActivity(), BookmarkedStatusListFragment.class, args);
|
||||||
|
}else if(id==R.id.favorites){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
|
||||||
}else if(id==R.id.manage_user_lists){
|
}else if(id==R.id.manage_user_lists){
|
||||||
final Bundle args=new Bundle();
|
final Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
@@ -667,7 +661,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
||||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||||
notifyButton.setSelected(relationship.notifying);
|
notifyButton.setSelected(relationship.notifying);
|
||||||
if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username));
|
if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.animation.AlphaAnimation;
|
|
||||||
import android.view.animation.LinearInterpolator;
|
import android.view.animation.LinearInterpolator;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@@ -36,6 +35,7 @@ import org.joinmastodon.android.MainActivity;
|
|||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
|
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
@@ -69,6 +69,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
private NotificationPolicyItem notificationPolicyItem;
|
private NotificationPolicyItem notificationPolicyItem;
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private boolean needUpdateNotificationSettings;
|
private boolean needUpdateNotificationSettings;
|
||||||
|
private boolean needAppRestart;
|
||||||
private PushSubscription pushSubscription;
|
private PushSubscription pushSubscription;
|
||||||
|
|
||||||
private ImageView themeTransitionWindowView;
|
private ImageView themeTransitionWindowView;
|
||||||
@@ -94,10 +95,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
items.add(new HeaderItem(R.string.settings_theme));
|
items.add(new HeaderItem(R.string.settings_theme));
|
||||||
items.add(themeItem=new ThemeItem());
|
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 SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||||
items.add(new SwitchItem(R.string.disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{
|
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.disableMarquee=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
|
items.add(new ColorPicker());
|
||||||
|
|
||||||
items.add(new HeaderItem(R.string.settings_behavior));
|
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->{
|
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||||
@@ -108,28 +110,33 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
GlobalUserPreferences.useCustomTabs=i.checked;
|
GlobalUserPreferences.useCustomTabs=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{
|
items.add(new SwitchItem(R.string.sk_settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{
|
||||||
GlobalUserPreferences.showInteractionCounts=i.checked;
|
GlobalUserPreferences.showInteractionCounts=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
|
items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
|
||||||
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
|
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
items.add(new HeaderItem(R.string.home_timeline));
|
items.add(new HeaderItem(R.string.home_timeline));
|
||||||
items.add(new SwitchItem(R.string.settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||||
GlobalUserPreferences.showReplies=i.checked;
|
GlobalUserPreferences.showReplies=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
|
items.add(new SwitchItem(R.string.sk_settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
|
||||||
GlobalUserPreferences.showBoosts=i.checked;
|
GlobalUserPreferences.showBoosts=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
||||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||||
GlobalUserPreferences.save();
|
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(new HeaderItem(R.string.settings_notifications));
|
||||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||||
@@ -138,6 +145,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
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_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_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_at_symbol, 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 HeaderItem(R.string.settings_boring));
|
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_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
|
||||||
@@ -146,14 +154,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
|
|
||||||
items.add(new RedHeaderItem(R.string.settings_spicy));
|
items.add(new RedHeaderItem(R.string.settings_spicy));
|
||||||
if (GithubSelfUpdater.needSelfUpdating()) {
|
if (GithubSelfUpdater.needSelfUpdating()) {
|
||||||
checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
||||||
items.add(checkForUpdateItem);
|
items.add(checkForUpdateItem);
|
||||||
}
|
}
|
||||||
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
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));
|
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||||
|
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.clear();
|
||||||
|
GlobalUserPreferences.save();
|
||||||
|
})));
|
||||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||||
|
|
||||||
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -198,9 +210,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy(){
|
public void onDestroy(){
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
if(needUpdateNotificationSettings){
|
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -223,6 +240,12 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
restartActivityToApplyNewTheme();
|
restartActivityToApplyNewTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onColorPreferenceClick(GlobalUserPreferences.ColorPreference color){
|
||||||
|
GlobalUserPreferences.color=color;
|
||||||
|
GlobalUserPreferences.save();
|
||||||
|
restartActivityToApplyNewTheme();
|
||||||
|
}
|
||||||
|
|
||||||
private void onTrueBlackThemeChanged(SwitchItem item){
|
private void onTrueBlackThemeChanged(SwitchItem item){
|
||||||
GlobalUserPreferences.trueBlackTheme=item.checked;
|
GlobalUserPreferences.trueBlackTheme=item.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
@@ -282,6 +305,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
case FOLLOW -> subscription.alerts.follow=enabled;
|
case FOLLOW -> subscription.alerts.follow=enabled;
|
||||||
case REBLOG -> subscription.alerts.reblog=enabled;
|
case REBLOG -> subscription.alerts.reblog=enabled;
|
||||||
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
|
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
|
||||||
|
case STATUS -> subscription.alerts.status=enabled;
|
||||||
}
|
}
|
||||||
needUpdateNotificationSettings=true;
|
needUpdateNotificationSettings=true;
|
||||||
}
|
}
|
||||||
@@ -376,7 +400,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
||||||
Toast.makeText(getActivity(), R.string.no_update_available, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getActivity(), R.string.sk_no_update_available, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +449,13 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ColorPicker extends Item{
|
||||||
|
@Override
|
||||||
|
public int getViewType(){
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class ThemeItem extends Item{
|
private static class ThemeItem extends Item{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -509,6 +540,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
case 5 -> new HeaderViewHolder(true);
|
case 5 -> new HeaderViewHolder(true);
|
||||||
case 6 -> new FooterViewHolder();
|
case 6 -> new FooterViewHolder();
|
||||||
case 7 -> new UpdateViewHolder();
|
case 7 -> new UpdateViewHolder();
|
||||||
|
case 8 -> new ColorPickerViewHolder();
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -637,6 +669,68 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private class ColorPickerViewHolder extends BindableViewHolder<ColorPicker>{
|
||||||
|
private final Button button;
|
||||||
|
private final PopupMenu popupMenu;
|
||||||
|
private final ImageView icon;
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
public ColorPickerViewHolder(){
|
||||||
|
super(getActivity(), R.layout.item_settings_color_picker, list);
|
||||||
|
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(item->{
|
||||||
|
GlobalUserPreferences.ColorPreference pref;
|
||||||
|
int id=item.getItemId();
|
||||||
|
if(id==R.id.pink_color) {
|
||||||
|
pref = GlobalUserPreferences.ColorPreference.PINK;
|
||||||
|
onColorPreferenceClick(pref);
|
||||||
|
}
|
||||||
|
else if(id==R.id.purple_color) {
|
||||||
|
pref = GlobalUserPreferences.ColorPreference.PURPLE;
|
||||||
|
onColorPreferenceClick(pref);
|
||||||
|
}
|
||||||
|
else if(id==R.id.green_color) {
|
||||||
|
pref = GlobalUserPreferences.ColorPreference.GREEN;
|
||||||
|
onColorPreferenceClick(pref);
|
||||||
|
}
|
||||||
|
else if(id==R.id.blue_color) {
|
||||||
|
pref = GlobalUserPreferences.ColorPreference.BLUE;
|
||||||
|
onColorPreferenceClick(pref);
|
||||||
|
}
|
||||||
|
else if(id==R.id.brown_color) {
|
||||||
|
pref = GlobalUserPreferences.ColorPreference.BROWN;
|
||||||
|
onColorPreferenceClick(pref);
|
||||||
|
}
|
||||||
|
else if(id==R.id.yellow_color) {
|
||||||
|
pref = GlobalUserPreferences.ColorPreference.YELLOW;
|
||||||
|
onColorPreferenceClick(pref);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
// UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
|
||||||
|
button.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||||
|
button.setOnClickListener(v->popupMenu.show());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBind(ColorPicker item){
|
||||||
|
icon.setImageResource(R.drawable.ic_fluent_color_24_regular);
|
||||||
|
button.setText(switch(GlobalUserPreferences.color){
|
||||||
|
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 YELLOW -> R.string.sk_color_theme_yellow;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
|
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
|
||||||
private final Button button;
|
private final Button button;
|
||||||
@@ -752,10 +846,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
|||||||
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
|
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
|
||||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||||
text.setText(getString(R.string.update_available, info.version));
|
text.setText(getString(R.string.sk_update_available, info.version));
|
||||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||||
}else{
|
}else{
|
||||||
text.setText(getString(R.string.update_ready, info.version));
|
text.setText(getString(R.string.sk_update_ready, info.version));
|
||||||
button.setText(R.string.install_update);
|
button.setText(R.string.install_update);
|
||||||
}
|
}
|
||||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.style.ImageSpan;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.MastodonApp;
|
import org.joinmastodon.android.MastodonApp;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogFragment;
|
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
|
||||||
|
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
|
||||||
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
|
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
|
||||||
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.fragments.AppKitFragment;
|
import me.grishka.appkit.fragments.AppKitFragment;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
@@ -23,12 +35,13 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
|
|
||||||
private SizeListenerFrameLayout contentView;
|
private SizeListenerFrameLayout contentView;
|
||||||
private View artContainer, blueFill, greenFill;
|
private View artContainer, blueFill, greenFill;
|
||||||
private InterpolatingMotionEffect motionEffect;
|
private ViewPager2 pager;
|
||||||
|
private ViewGroup pagerDots;
|
||||||
|
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -37,15 +50,44 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
|
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
|
||||||
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
|
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
|
||||||
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
|
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
|
||||||
|
artClouds=contentView.findViewById(R.id.art_clouds);
|
||||||
|
artPlaneElephant=contentView.findViewById(R.id.art_plane_elephant);
|
||||||
|
artRightHill=contentView.findViewById(R.id.art_right_hill);
|
||||||
|
artLeftHill=contentView.findViewById(R.id.art_left_hill);
|
||||||
|
artCenterHill=contentView.findViewById(R.id.art_center_hill);
|
||||||
|
pager=contentView.findViewById(R.id.pager);
|
||||||
|
pagerDots=contentView.findViewById(R.id.pager_dots);
|
||||||
|
pager.setAdapter(new PagerAdapter());
|
||||||
|
pager.setOffscreenPageLimit(3);
|
||||||
|
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||||
|
@Override
|
||||||
|
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
|
||||||
|
for(int i=0;i<pagerDots.getChildCount();i++){
|
||||||
|
float alpha;
|
||||||
|
if(i==position){
|
||||||
|
alpha=0.3f+0.7f*(1f-positionOffset);
|
||||||
|
}else if(i==position+1){
|
||||||
|
alpha=0.3f+0.7f*positionOffset;
|
||||||
|
}else{
|
||||||
|
alpha=0.3f;
|
||||||
|
}
|
||||||
|
pagerDots.getChildAt(i).setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
float parallaxProgress=(position+positionOffset)/2f;
|
||||||
|
artClouds.setTranslationX(V.dp(-27)*(position>=1 ? 1f : positionOffset));
|
||||||
|
artPlaneElephant.setTranslationX(V.dp(101.55f)*parallaxProgress);
|
||||||
|
artLeftHill.setTranslationX(V.dp(-88)*parallaxProgress);
|
||||||
|
artLeftHill.setTranslationY(V.dp(24)*parallaxProgress);
|
||||||
|
artRightHill.setTranslationX(V.dp(-88)*parallaxProgress);
|
||||||
|
artRightHill.setTranslationY(V.dp(-24)*parallaxProgress);
|
||||||
|
artCenterHill.setTranslationX(V.dp(-40)*parallaxProgress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
artContainer=contentView.findViewById(R.id.art_container);
|
artContainer=contentView.findViewById(R.id.art_container);
|
||||||
blueFill=contentView.findViewById(R.id.blue_fill);
|
blueFill=contentView.findViewById(R.id.blue_fill);
|
||||||
greenFill=contentView.findViewById(R.id.green_fill);
|
greenFill=contentView.findViewById(R.id.green_fill);
|
||||||
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_clouds), V.dp(-5), V.dp(5), V.dp(-5), V.dp(5)));
|
|
||||||
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_right_hill), V.dp(-15), V.dp(25), V.dp(-10), V.dp(10)));
|
|
||||||
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_left_hill), V.dp(-25), V.dp(15), V.dp(-15), V.dp(15)));
|
|
||||||
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_center_hill), V.dp(-14), V.dp(14), V.dp(-5), V.dp(25)));
|
|
||||||
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_plane_elephant), V.dp(-20), V.dp(12), V.dp(-20), V.dp(12)));
|
|
||||||
|
|
||||||
contentView.setSizeListener(new SizeListenerFrameLayout.OnSizeChangedListener(){
|
contentView.setSizeListener(new SizeListenerFrameLayout.OnSizeChangedListener(){
|
||||||
@Override
|
@Override
|
||||||
@@ -66,15 +108,16 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
|
|
||||||
private void onButtonClick(View v){
|
private void onButtonClick(View v){
|
||||||
Bundle extras=new Bundle();
|
Bundle extras=new Bundle();
|
||||||
extras.putBoolean("signup", v.getId()==R.id.btn_get_started);
|
boolean isSignup=v.getId()==R.id.btn_get_started;
|
||||||
Nav.go(getActivity(), InstanceCatalogFragment.class, extras);
|
extras.putBoolean("signup", isSignup);
|
||||||
|
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateArtSize(int w, int h){
|
private void updateArtSize(int w, int h){
|
||||||
float scale=w/(float)V.dp(412);
|
float scale=w/(float)V.dp(360);
|
||||||
artContainer.setScaleX(scale);
|
artContainer.setScaleX(scale);
|
||||||
artContainer.setScaleY(scale);
|
artContainer.setScaleY(scale);
|
||||||
blueFill.setScaleY(h/2f);
|
blueFill.setScaleY(artContainer.getBottom()-V.dp(90));
|
||||||
greenFill.setScaleY(h-artContainer.getBottom()+V.dp(90));
|
greenFill.setScaleY(h-artContainer.getBottom()+V.dp(90));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,15 +143,91 @@ public class SplashFragment extends AppKitFragment{
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder>{
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
protected void onShown(){
|
public PagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||||
super.onShown();
|
return new PagerViewHolder(viewType);
|
||||||
motionEffect.activate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onHidden(){
|
public void onBindViewHolder(@NonNull PagerViewHolder holder, int position){}
|
||||||
super.onHidden();
|
|
||||||
motionEffect.deactivate();
|
@Override
|
||||||
|
public int getItemCount(){
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position){
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PagerViewHolder extends RecyclerView.ViewHolder{
|
||||||
|
public PagerViewHolder(int page){
|
||||||
|
super(new LinearLayout(getActivity()));
|
||||||
|
LinearLayout ll=(LinearLayout) itemView;
|
||||||
|
ll.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
int pad=V.dp(16);
|
||||||
|
ll.setPadding(pad, pad, pad, pad);
|
||||||
|
ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
TextView title=new TextView(getActivity());
|
||||||
|
title.setTextAppearance(R.style.m3_headline_medium);
|
||||||
|
title.setText(switch(page){
|
||||||
|
case 0 -> {
|
||||||
|
String src=getString(R.string.welcome_page1_title);
|
||||||
|
SpannableString ss=new SpannableString(src);
|
||||||
|
int start=src.indexOf("{logo}");
|
||||||
|
if(start!=-1){
|
||||||
|
LogoSpan span=new LogoSpan(getResources().getDrawable(R.drawable.splash_logo, getActivity().getTheme()));
|
||||||
|
ss.setSpan(span, start, start+6, 0);
|
||||||
|
}
|
||||||
|
yield ss;
|
||||||
|
}
|
||||||
|
case 1 -> getString(R.string.welcome_page2_title);
|
||||||
|
case 2 -> getString(R.string.welcome_page3_title);
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||||
|
});
|
||||||
|
title.setTextColor(0xFF17063B);
|
||||||
|
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(page==0 ? 46 : 36));
|
||||||
|
lp.bottomMargin=V.dp(page==0 ? 4 : 14);
|
||||||
|
ll.addView(title, lp);
|
||||||
|
|
||||||
|
TextView text=new TextView(getActivity());
|
||||||
|
text.setTextAppearance(R.style.m3_body_medium);
|
||||||
|
text.setText(switch(page){
|
||||||
|
case 0 -> R.string.welcome_page1_text;
|
||||||
|
case 1 -> R.string.welcome_page2_text;
|
||||||
|
case 2 -> R.string.welcome_page3_text;
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||||
|
});
|
||||||
|
text.setTextColor(0xFF17063B);
|
||||||
|
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LogoSpan extends ReplacementSpan{
|
||||||
|
private final Drawable drawable;
|
||||||
|
|
||||||
|
private LogoSpan(Drawable drawable){
|
||||||
|
this.drawable=drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
|
||||||
|
return drawable.getIntrinsicWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
|
||||||
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(x, y-V.dp(20));
|
||||||
|
drawable.draw(canvas);
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.squareup.otto.Subscribe;
|
|||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||||
@@ -18,6 +19,8 @@ import org.parceler.Parcels;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
@@ -134,6 +137,40 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
|
List<Status> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
||||||
|
.filter(s->s.account.id.equals(ev.postsByAccountID) || (s.reblog!=null && s.reblog.account.id.equals(ev.postsByAccountID)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
for(Status s:toRemove){
|
||||||
|
removeStatus(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removeStatus(Status 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);
|
||||||
|
}
|
||||||
|
|
||||||
public class EventListener{
|
public class EventListener{
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
@@ -165,28 +202,13 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
|||||||
Status status=getStatusByID(ev.id);
|
Status status=getStatusByID(ev.id);
|
||||||
if(status==null)
|
if(status==null)
|
||||||
return;
|
return;
|
||||||
data.remove(status);
|
removeStatus(status);
|
||||||
preloadedData.remove(status);
|
|
||||||
int index=-1;
|
|
||||||
for(int i=0;i<displayItems.size();i++){
|
|
||||||
if(ev.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(ev.id))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
displayItems.subList(index, lastIndex).clear();
|
|
||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onStatusCreated(StatusCreatedEvent ev){
|
public void onStatusCreated(StatusCreatedEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
StatusListFragment.this.onStatusCreated(ev);
|
StatusListFragment.this.onStatusCreated(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,5 +228,14 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
|
if(!ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
if(ev.isUnfollow && !shouldRemoveAccountPostsWhenUnfollowing())
|
||||||
|
return;
|
||||||
|
StatusListFragment.this.onRemoveAccountPostsEvent(ev);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,13 +286,16 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
|||||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_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.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.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.lists_with_user, account.getDisplayUsername()));
|
|
||||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||||
|
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
|
||||||
if(relationship.following){
|
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.getDisplayUsername()));
|
||||||
hideBoosts.setVisible(true);
|
hideBoosts.setVisible(true);
|
||||||
|
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername()));
|
||||||
|
manageUserLists.setVisible(true);
|
||||||
}else{
|
}else{
|
||||||
hideBoosts.setVisible(false);
|
hideBoosts.setVisible(false);
|
||||||
|
manageUserLists.setVisible(true);
|
||||||
}
|
}
|
||||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||||
if(!account.isLocal()){
|
if(!account.isLocal()){
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.joinmastodon.android.fragments.discover;
|
package org.joinmastodon.android.fragments.discover;
|
||||||
|
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
|
import android.app.FragmentTransaction;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
@@ -17,6 +18,8 @@ import android.widget.LinearLayout;
|
|||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.BuildConfig;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||||
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
||||||
@@ -29,6 +32,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
|
||||||
import me.grishka.appkit.fragments.AppKitFragment;
|
import me.grishka.appkit.fragments.AppKitFragment;
|
||||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
@@ -58,6 +62,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
private String accountID;
|
private String accountID;
|
||||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||||
|
|
||||||
|
private final boolean noFederated = !GlobalUserPreferences.showFederatedTimeline;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -75,10 +81,11 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
tabLayout=view.findViewById(R.id.tabbar);
|
tabLayout=view.findViewById(R.id.tabbar);
|
||||||
pager=view.findViewById(R.id.pager);
|
pager=view.findViewById(R.id.pager);
|
||||||
|
|
||||||
tabViews=new FrameLayout[7];
|
tabViews=new FrameLayout[noFederated ? 6 : 7];
|
||||||
for(int i=0;i<tabViews.length;i++){
|
for(int i=0;i<tabViews.length;i++){
|
||||||
FrameLayout tabView=new FrameLayout(getActivity());
|
FrameLayout tabView=new FrameLayout(getActivity());
|
||||||
tabView.setId(switch(i){
|
int switchIndex = noFederated && i > 0 ? i + 1 : i;
|
||||||
|
tabView.setId(switch(switchIndex){
|
||||||
case 0 -> R.id.discover_local_timeline;
|
case 0 -> R.id.discover_local_timeline;
|
||||||
case 1 -> R.id.discover_federated_timeline;
|
case 1 -> R.id.discover_federated_timeline;
|
||||||
case 2 -> R.id.discover_hashtags;
|
case 2 -> R.id.discover_hashtags;
|
||||||
@@ -86,7 +93,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
case 4 -> R.id.discover_news;
|
case 4 -> R.id.discover_news;
|
||||||
case 5 -> R.id.discover_users;
|
case 5 -> R.id.discover_users;
|
||||||
case 6 -> R.id.discover_lists;
|
case 6 -> R.id.discover_lists;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
default -> throw new IllegalStateException("Unexpected value: "+switchIndex);
|
||||||
});
|
});
|
||||||
tabView.setVisibility(View.GONE);
|
tabView.setVisibility(View.GONE);
|
||||||
view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
|
view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
|
||||||
@@ -131,34 +138,38 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
localTimelineFragment=new LocalTimelineFragment();
|
localTimelineFragment=new LocalTimelineFragment();
|
||||||
localTimelineFragment.setArguments(args);
|
localTimelineFragment.setArguments(args);
|
||||||
|
|
||||||
federatedTimelineFragment=new FederatedTimelineFragment();
|
|
||||||
federatedTimelineFragment.setArguments(args);
|
|
||||||
|
|
||||||
listTimelinesFragment=new ListTimelinesFragment();
|
listTimelinesFragment=new ListTimelinesFragment();
|
||||||
listTimelinesFragment.setArguments(args);
|
listTimelinesFragment.setArguments(args);
|
||||||
|
|
||||||
getChildFragmentManager().beginTransaction()
|
FragmentTransaction transaction = getChildFragmentManager().beginTransaction()
|
||||||
.add(R.id.discover_posts, postsFragment)
|
.add(R.id.discover_posts, postsFragment)
|
||||||
.add(R.id.discover_local_timeline, localTimelineFragment)
|
.add(R.id.discover_local_timeline, localTimelineFragment)
|
||||||
.add(R.id.discover_federated_timeline, federatedTimelineFragment)
|
|
||||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||||
.add(R.id.discover_news, newsFragment)
|
.add(R.id.discover_news, newsFragment)
|
||||||
.add(R.id.discover_users, accountsFragment)
|
.add(R.id.discover_users, accountsFragment)
|
||||||
.add(R.id.discover_lists, listTimelinesFragment)
|
.add(R.id.discover_lists, listTimelinesFragment);
|
||||||
.commit();
|
|
||||||
|
if (!noFederated) {
|
||||||
|
federatedTimelineFragment=new FederatedTimelineFragment();
|
||||||
|
federatedTimelineFragment.setArguments(args);
|
||||||
|
transaction.add(R.id.discover_federated_timeline, federatedTimelineFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){
|
tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){
|
||||||
@Override
|
@Override
|
||||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||||
|
if (noFederated && position > 0) position++;
|
||||||
tab.setText(switch(position){
|
tab.setText(switch(position){
|
||||||
case 0 -> R.string.local_timeline;
|
case 0 -> R.string.local_timeline;
|
||||||
case 1 -> R.string.federated_timeline;
|
case 1 -> R.string.sk_federated_timeline;
|
||||||
case 2 -> R.string.hashtags;
|
case 2 -> R.string.hashtags;
|
||||||
case 3 -> R.string.posts;
|
case 3 -> R.string.posts;
|
||||||
case 4 -> R.string.news;
|
case 4 -> R.string.news;
|
||||||
case 5 -> R.string.for_you;
|
case 5 -> R.string.for_you;
|
||||||
case 6 -> R.string.list_timelines;
|
case 6 -> R.string.sk_list_timelines;
|
||||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||||
});
|
});
|
||||||
tab.view.textView.setAllCaps(true);
|
tab.view.textView.setAllCaps(true);
|
||||||
@@ -280,6 +291,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Fragment getFragmentForPage(int page){
|
private Fragment getFragmentForPage(int page){
|
||||||
|
if (noFederated && page > 0) page++;
|
||||||
return switch(page){
|
return switch(page){
|
||||||
case 0 -> localTimelineFragment;
|
case 0 -> localTimelineFragment;
|
||||||
case 1 -> federatedTimelineFragment;
|
case 1 -> federatedTimelineFragment;
|
||||||
|
|||||||
@@ -2,46 +2,30 @@ package org.joinmastodon.android.fragments.onboarding;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.LocaleList;
|
import android.os.LocaleList;
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.RadioButton;
|
import android.widget.RadioButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
|
||||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
|
|
||||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.catalog.CatalogCategory;
|
|
||||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
|
||||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.parceler.Parcels;
|
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.Node;
|
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
import org.xml.sax.InputSource;
|
import org.xml.sax.InputSource;
|
||||||
|
|
||||||
@@ -59,49 +43,42 @@ import java.util.stream.Collectors;
|
|||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.DiffUtil;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
|
||||||
public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
|
abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
|
||||||
private InstancesAdapter adapter;
|
protected RecyclerView.Adapter adapter;
|
||||||
private MergeRecyclerAdapter mergeAdapter;
|
protected MergeRecyclerAdapter mergeAdapter;
|
||||||
private View headerView;
|
protected CatalogInstance chosenInstance;
|
||||||
private CatalogInstance chosenInstance;
|
protected Button nextButton;
|
||||||
private List<CatalogInstance> filteredData=new ArrayList<>();
|
protected EditText searchEdit;
|
||||||
private Button nextButton;
|
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||||
private MastodonAPIRequest<?> getCategoriesRequest;
|
protected String currentSearchQuery;
|
||||||
private EditText searchEdit;
|
protected String loadingInstanceDomain;
|
||||||
private TabLayout categoriesList;
|
protected HashMap<String, Instance> instancesCache=new HashMap<>();
|
||||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
protected View buttonBar;
|
||||||
private String currentSearchQuery;
|
protected List<CatalogInstance> filteredData=new ArrayList<>();
|
||||||
private String currentCategory="all";
|
protected GetInstance loadingInstanceRequest;
|
||||||
private List<CatalogCategory> categories=new ArrayList<>();
|
protected Call loadingInstanceRedirectRequest;
|
||||||
private String loadingInstanceDomain;
|
protected ProgressDialog instanceProgressDialog;
|
||||||
private GetInstance loadingInstanceRequest;
|
protected HashMap<String, String> redirects=new HashMap<>();
|
||||||
private Call loadingInstanceRedirectRequest;
|
protected HashMap<String, String> redirectsInverse=new HashMap<>();
|
||||||
private HashMap<String, Instance> instancesCache=new HashMap<>();
|
protected boolean isSignup;
|
||||||
private ProgressDialog instanceProgressDialog;
|
protected CatalogInstance fakeInstance=new CatalogInstance();
|
||||||
private View buttonBar;
|
|
||||||
private HashMap<String, String> redirects=new HashMap<>(), redirectsInverse=new HashMap<>();
|
|
||||||
|
|
||||||
private boolean isSignup;
|
|
||||||
|
|
||||||
private static final double DUNBAR=Math.log(800);
|
private static final double DUNBAR=Math.log(800);
|
||||||
|
|
||||||
public InstanceCatalogFragment(){
|
public InstanceCatalogFragment(int layout, int perPage){
|
||||||
super(R.layout.fragment_onboarding_common, 10);
|
super(layout, perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -110,21 +87,31 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||||||
isSignup=getArguments().getBoolean("signup");
|
isSignup=getArguments().getBoolean("signup");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected abstract void proceedWithAuthOrSignup(Instance instance);
|
||||||
public void onAttach(Context context){
|
|
||||||
super.onAttach(context);
|
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
|
||||||
setRefreshEnabled(false);
|
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
||||||
loadData();
|
return true;
|
||||||
|
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||||
|
updateFilteredList();
|
||||||
|
searchEdit.removeCallbacks(searchDebouncer);
|
||||||
|
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
|
||||||
|
if(instance==null){
|
||||||
|
showProgressDialog();
|
||||||
|
loadInstanceInfo(currentSearchQuery, false);
|
||||||
|
}else{
|
||||||
|
proceedWithAuthOrSignup(instance);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected void onSearchChangedDebounced(){
|
||||||
protected void doLoadData(int offset, int count){
|
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||||
currentRequest=new GetCatalogInstances(null, null)
|
updateFilteredList();
|
||||||
.setCallback(new Callback<>(){
|
loadInstanceInfo(currentSearchQuery, false);
|
||||||
@Override
|
}
|
||||||
public void onSuccess(List<CatalogInstance> result){
|
|
||||||
if(getActivity()==null)
|
protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){
|
||||||
return;
|
|
||||||
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
|
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
|
||||||
for(List<CatalogInstance> group:byLang.values()){
|
for(List<CatalogInstance> group:byLang.values()){
|
||||||
Collections.sort(group, (a, b)->{
|
Collections.sort(group, (a, b)->{
|
||||||
@@ -165,280 +152,26 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||||||
}
|
}
|
||||||
return group;
|
return group;
|
||||||
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
|
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
|
||||||
onDataLoaded(sortedList, false);
|
return sortedList;
|
||||||
updateFilteredList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected abstract void updateFilteredList();
|
||||||
public void onError(ErrorResponse error){
|
|
||||||
error.showToast(getActivity());
|
|
||||||
onDataLoaded(Collections.emptyList(), false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.execNoAuth("");
|
|
||||||
getCategoriesRequest=new GetCatalogCategories(null)
|
|
||||||
.setCallback(new Callback<>(){
|
|
||||||
@Override
|
|
||||||
public void onSuccess(List<CatalogCategory> result){
|
|
||||||
getCategoriesRequest=null;
|
|
||||||
CatalogCategory all=new CatalogCategory();
|
|
||||||
all.category="all";
|
|
||||||
categories.add(all);
|
|
||||||
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
|
|
||||||
updateCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
protected void showProgressDialog(){
|
||||||
public void onError(ErrorResponse error){
|
|
||||||
getCategoriesRequest=null;
|
|
||||||
error.showToast(getActivity());
|
|
||||||
CatalogCategory all=new CatalogCategory();
|
|
||||||
all.category="all";
|
|
||||||
categories.add(all);
|
|
||||||
updateCategories();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.execNoAuth("");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCategories(){
|
|
||||||
categoriesList.removeAllTabs();
|
|
||||||
for(CatalogCategory cat:categories){
|
|
||||||
int titleRes=getTitleForCategory(cat.category);
|
|
||||||
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
|
|
||||||
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
|
|
||||||
emoji.setImageResource(getEmojiForCategory(cat.category));
|
|
||||||
categoriesList.addTab(tab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy(){
|
|
||||||
super.onDestroy();
|
|
||||||
if(getCategoriesRequest!=null)
|
|
||||||
getCategoriesRequest.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RecyclerView.Adapter getAdapter(){
|
|
||||||
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
|
|
||||||
searchEdit=headerView.findViewById(R.id.search_edit);
|
|
||||||
categoriesList=headerView.findViewById(R.id.categories_list);
|
|
||||||
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
|
||||||
@Override
|
|
||||||
public void onTabSelected(TabLayout.Tab tab){
|
|
||||||
CatalogCategory category=categories.get(tab.getPosition());
|
|
||||||
currentCategory=category.category;
|
|
||||||
updateFilteredList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTabUnselected(TabLayout.Tab tab){
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTabReselected(TabLayout.Tab tab){
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
|
|
||||||
searchEdit.addTextChangedListener(new TextWatcher(){
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
|
||||||
searchEdit.removeCallbacks(searchDebouncer);
|
|
||||||
searchEdit.postDelayed(searchDebouncer, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable s){
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mergeAdapter=new MergeRecyclerAdapter();
|
|
||||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
|
||||||
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
|
|
||||||
return mergeAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
|
||||||
super.onViewCreated(view, savedInstanceState);
|
|
||||||
nextButton=view.findViewById(R.id.btn_next);
|
|
||||||
nextButton.setOnClickListener(this::onNextClick);
|
|
||||||
nextButton.setEnabled(chosenInstance!=null);
|
|
||||||
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
|
|
||||||
list.setItemAnimator(new BetterItemAnimator());
|
|
||||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
|
|
||||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
|
||||||
buttonBar=view.findViewById(R.id.button_bar);
|
|
||||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNextClick(View v){
|
|
||||||
String domain=chosenInstance.domain;
|
|
||||||
Instance instance=instancesCache.get(domain);
|
|
||||||
if(instance!=null){
|
|
||||||
proceedWithAuthOrSignup(instance);
|
|
||||||
}else{
|
|
||||||
showProgressDialog();
|
|
||||||
if(!domain.equals(loadingInstanceDomain)){
|
|
||||||
loadInstanceInfo(domain, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void proceedWithAuthOrSignup(Instance instance){
|
|
||||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
|
||||||
if(isSignup){
|
|
||||||
if(!instance.registrations){
|
|
||||||
new M3AlertDialogBuilder(getActivity())
|
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(R.string.instance_signup_closed)
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putParcelable("instance", Parcels.wrap(instance));
|
|
||||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
|
||||||
}else{
|
|
||||||
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// private String getEmojiForCategory(String category){
|
|
||||||
// return switch(category){
|
|
||||||
// case "all" -> "💬";
|
|
||||||
// case "academia" -> "📚";
|
|
||||||
// case "activism" -> "✊";
|
|
||||||
// case "food" -> "🍕";
|
|
||||||
// case "furry" -> "🦁";
|
|
||||||
// case "games" -> "🕹";
|
|
||||||
// case "general" -> "🐘";
|
|
||||||
// case "journalism" -> "📰";
|
|
||||||
// case "lgbt" -> "🏳️🌈";
|
|
||||||
// case "regional" -> "📍";
|
|
||||||
// case "art" -> "🎨";
|
|
||||||
// case "music" -> "🎼";
|
|
||||||
// case "tech" -> "📱";
|
|
||||||
// default -> "❓";
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
private int getEmojiForCategory(String category){
|
|
||||||
return switch(category){
|
|
||||||
case "all" -> R.drawable.ic_category_all;
|
|
||||||
case "academia" -> R.drawable.ic_category_academia;
|
|
||||||
case "activism" -> R.drawable.ic_category_activism;
|
|
||||||
case "food" -> R.drawable.ic_category_food;
|
|
||||||
case "furry" -> R.drawable.ic_category_furry;
|
|
||||||
case "games" -> R.drawable.ic_category_games;
|
|
||||||
case "general" -> R.drawable.ic_category_general;
|
|
||||||
case "journalism" -> R.drawable.ic_category_journalism;
|
|
||||||
case "lgbt" -> R.drawable.ic_category_lgbt;
|
|
||||||
case "regional" -> R.drawable.ic_category_regional;
|
|
||||||
case "art" -> R.drawable.ic_category_art;
|
|
||||||
case "music" -> R.drawable.ic_category_music;
|
|
||||||
case "tech" -> R.drawable.ic_category_tech;
|
|
||||||
default -> R.drawable.ic_category_unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getTitleForCategory(String category){
|
|
||||||
return switch(category){
|
|
||||||
case "all" -> R.string.category_all;
|
|
||||||
case "academia" -> R.string.category_academia;
|
|
||||||
case "activism" -> R.string.category_activism;
|
|
||||||
case "food" -> R.string.category_food;
|
|
||||||
case "furry" -> R.string.category_furry;
|
|
||||||
case "games" -> R.string.category_games;
|
|
||||||
case "general" -> R.string.category_general;
|
|
||||||
case "journalism" -> R.string.category_journalism;
|
|
||||||
case "lgbt" -> R.string.category_lgbt;
|
|
||||||
case "regional" -> R.string.category_regional;
|
|
||||||
case "art" -> R.string.category_art;
|
|
||||||
case "music" -> R.string.category_music;
|
|
||||||
case "tech" -> R.string.category_tech;
|
|
||||||
default -> 0;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
|
|
||||||
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
|
||||||
return true;
|
|
||||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
|
||||||
updateFilteredList();
|
|
||||||
searchEdit.removeCallbacks(searchDebouncer);
|
|
||||||
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
|
|
||||||
if(instance==null){
|
|
||||||
showProgressDialog();
|
|
||||||
loadInstanceInfo(currentSearchQuery, false);
|
|
||||||
}else{
|
|
||||||
proceedWithAuthOrSignup(instance);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSearchChangedDebounced(){
|
|
||||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
|
||||||
updateFilteredList();
|
|
||||||
loadInstanceInfo(currentSearchQuery, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFilteredList(){
|
|
||||||
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
|
|
||||||
filteredData.clear();
|
|
||||||
for(CatalogInstance instance:data){
|
|
||||||
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
|
|
||||||
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
|
|
||||||
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
|
|
||||||
filteredData.add(instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DiffUtil.calculateDiff(new DiffUtil.Callback(){
|
|
||||||
@Override
|
|
||||||
public int getOldListSize(){
|
|
||||||
return prevData.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getNewListSize(){
|
|
||||||
return filteredData.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
|
|
||||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
|
|
||||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
|
||||||
}
|
|
||||||
}).dispatchUpdatesTo(adapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showProgressDialog(){
|
|
||||||
instanceProgressDialog=new ProgressDialog(getActivity());
|
instanceProgressDialog=new ProgressDialog(getActivity());
|
||||||
instanceProgressDialog.setMessage(getString(R.string.loading_instance));
|
instanceProgressDialog.setMessage(getString(R.string.loading_instance));
|
||||||
instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo());
|
instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo());
|
||||||
instanceProgressDialog.show();
|
instanceProgressDialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeInstanceDomain(String _domain){
|
protected String normalizeInstanceDomain(String _domain){
|
||||||
if(TextUtils.isEmpty(_domain))
|
if(TextUtils.isEmpty(_domain))
|
||||||
return null;
|
return null;
|
||||||
if(_domain.contains(":")){
|
if(_domain.contains(":")){
|
||||||
try{
|
try{
|
||||||
_domain=Uri.parse(_domain).getAuthority();
|
_domain=Uri.parse(_domain).getAuthority();
|
||||||
}catch(Exception ignore){}
|
}catch(Exception ignore){
|
||||||
|
}
|
||||||
if(TextUtils.isEmpty(_domain))
|
if(TextUtils.isEmpty(_domain))
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -453,12 +186,12 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadInstanceInfo(String _domain, boolean isFromRedirect){
|
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
|
||||||
String domain=normalizeInstanceDomain(_domain);
|
String domain=normalizeInstanceDomain(_domain);
|
||||||
Instance cachedInstance=instancesCache.get(domain);
|
Instance cachedInstance=instancesCache.get(domain);
|
||||||
if(cachedInstance!=null){
|
if(cachedInstance!=null){
|
||||||
for(CatalogInstance ci : filteredData){
|
for(CatalogInstance ci : filteredData){
|
||||||
if(ci.domain.equals(domain))
|
if(ci.domain.equals(domain) && ci!=fakeInstance)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CatalogInstance ci=cachedInstance.toCatalogInstance();
|
CatalogInstance ci=cachedInstance.toCatalogInstance();
|
||||||
@@ -490,18 +223,23 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||||||
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
|
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
|
||||||
boolean found=false;
|
boolean found=false;
|
||||||
for(CatalogInstance ci : filteredData){
|
for(CatalogInstance ci : filteredData){
|
||||||
if(ci.domain.equals(domain)){
|
if(ci.domain.equals(domain) && ci!=fakeInstance){
|
||||||
found=true;
|
found=true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!found){
|
if(!found){
|
||||||
CatalogInstance ci=result.toCatalogInstance();
|
CatalogInstance ci=result.toCatalogInstance();
|
||||||
|
if(filteredData.size()==1 && filteredData.get(0)==fakeInstance){
|
||||||
|
filteredData.set(0, ci);
|
||||||
|
adapter.notifyItemChanged(0);
|
||||||
|
}else{
|
||||||
filteredData.add(0, ci);
|
filteredData.add(0, ci);
|
||||||
adapter.notifyItemInserted(0);
|
adapter.notifyItemInserted(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
@@ -512,6 +250,14 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||||||
}
|
}
|
||||||
loadingInstanceDomain=null;
|
loadingInstanceDomain=null;
|
||||||
showInstanceInfoLoadError(domain, error);
|
showInstanceInfoLoadError(domain, error);
|
||||||
|
if(fakeInstance!=null){
|
||||||
|
fakeInstance.description=getString(R.string.error);
|
||||||
|
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
||||||
|
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
|
||||||
|
ivh.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).execNoAuth(domain);
|
}).execNoAuth(domain);
|
||||||
}
|
}
|
||||||
@@ -616,78 +362,26 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
|
|
||||||
public InstancesAdapter(){
|
|
||||||
super(imgLoader);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
return new InstanceViewHolder();
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
nextButton=view.findViewById(R.id.btn_next);
|
||||||
|
nextButton.setOnClickListener(this::onNextClick);
|
||||||
|
nextButton.setEnabled(chosenInstance!=null);
|
||||||
|
buttonBar=view.findViewById(R.id.button_bar);
|
||||||
|
setRefreshEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected void onNextClick(View v){
|
||||||
public void onBindViewHolder(InstanceViewHolder holder, int position){
|
String domain=chosenInstance.domain;
|
||||||
holder.bind(filteredData.get(position));
|
Instance instance=instancesCache.get(domain);
|
||||||
super.onBindViewHolder(holder, position);
|
if(instance!=null){
|
||||||
}
|
proceedWithAuthOrSignup(instance);
|
||||||
|
}else{
|
||||||
@Override
|
showProgressDialog();
|
||||||
public int getItemCount(){
|
if(!domain.equals(loadingInstanceDomain)){
|
||||||
return filteredData.size();
|
loadInstanceInfo(domain, false);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position){
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
|
|
||||||
private final TextView title, description, userCount, lang;
|
|
||||||
private final RadioButton radioButton;
|
|
||||||
|
|
||||||
public InstanceViewHolder(){
|
|
||||||
super(getActivity(), R.layout.item_instance_catalog, list);
|
|
||||||
title=findViewById(R.id.title);
|
|
||||||
description=findViewById(R.id.description);
|
|
||||||
userCount=findViewById(R.id.user_count);
|
|
||||||
lang=findViewById(R.id.lang);
|
|
||||||
radioButton=findViewById(R.id.radiobtn);
|
|
||||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
|
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBind(CatalogInstance item){
|
|
||||||
title.setText(item.normalizedDomain);
|
|
||||||
description.setText(item.description);
|
|
||||||
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
|
|
||||||
lang.setText(item.language.toUpperCase());
|
|
||||||
radioButton.setChecked(chosenInstance==item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(){
|
|
||||||
if(chosenInstance==item)
|
|
||||||
return;
|
|
||||||
if(chosenInstance!=null){
|
|
||||||
int idx=filteredData.indexOf(chosenInstance);
|
|
||||||
if(idx!=-1){
|
|
||||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
|
|
||||||
if(holder instanceof InstanceViewHolder ivh){
|
|
||||||
ivh.radioButton.setChecked(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
radioButton.setChecked(true);
|
|
||||||
if(chosenInstance==null)
|
|
||||||
nextButton.setEnabled(true);
|
|
||||||
chosenInstance=item;
|
|
||||||
loadInstanceInfo(chosenInstance.domain, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
package org.joinmastodon.android.fragments.onboarding;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.LocaleList;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
|
||||||
|
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.joinmastodon.android.model.catalog.CatalogCategory;
|
||||||
|
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||||
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
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.utils.BindableViewHolder;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
|
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
|
||||||
|
private View headerView;
|
||||||
|
private MastodonAPIRequest<?> getCategoriesRequest;
|
||||||
|
private TabLayout categoriesList;
|
||||||
|
private String currentCategory="all";
|
||||||
|
private List<CatalogCategory> categories=new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
public InstanceCatalogSignupFragment(){
|
||||||
|
super(R.layout.fragment_onboarding_common, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context){
|
||||||
|
super.onAttach(context);
|
||||||
|
setRefreshEnabled(false);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
currentRequest=new GetCatalogInstances(null, null)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<CatalogInstance> result){
|
||||||
|
if(getActivity()==null)
|
||||||
|
return;
|
||||||
|
onDataLoaded(sortInstances(result), false);
|
||||||
|
updateFilteredList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
onDataLoaded(Collections.emptyList(), false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execNoAuth("");
|
||||||
|
getCategoriesRequest=new GetCatalogCategories(null)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<CatalogCategory> result){
|
||||||
|
getCategoriesRequest=null;
|
||||||
|
CatalogCategory all=new CatalogCategory();
|
||||||
|
all.category="all";
|
||||||
|
categories.add(all);
|
||||||
|
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
|
||||||
|
updateCategories();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
getCategoriesRequest=null;
|
||||||
|
error.showToast(getActivity());
|
||||||
|
CatalogCategory all=new CatalogCategory();
|
||||||
|
all.category="all";
|
||||||
|
categories.add(all);
|
||||||
|
updateCategories();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execNoAuth("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCategories(){
|
||||||
|
categoriesList.removeAllTabs();
|
||||||
|
for(CatalogCategory cat:categories){
|
||||||
|
int titleRes=getTitleForCategory(cat.category);
|
||||||
|
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
|
||||||
|
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
|
||||||
|
emoji.setImageResource(getEmojiForCategory(cat.category));
|
||||||
|
categoriesList.addTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
if(getCategoriesRequest!=null)
|
||||||
|
getCategoriesRequest.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter getAdapter(){
|
||||||
|
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
|
||||||
|
searchEdit=headerView.findViewById(R.id.search_edit);
|
||||||
|
categoriesList=headerView.findViewById(R.id.categories_list);
|
||||||
|
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||||
|
@Override
|
||||||
|
public void onTabSelected(TabLayout.Tab tab){
|
||||||
|
CatalogCategory category=categories.get(tab.getPosition());
|
||||||
|
currentCategory=category.category;
|
||||||
|
updateFilteredList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTabUnselected(TabLayout.Tab tab){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTabReselected(TabLayout.Tab tab){
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
|
||||||
|
searchEdit.addTextChangedListener(new TextWatcher(){
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||||
|
searchEdit.removeCallbacks(searchDebouncer);
|
||||||
|
searchEdit.postDelayed(searchDebouncer, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s){
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mergeAdapter=new MergeRecyclerAdapter();
|
||||||
|
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||||
|
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
|
||||||
|
return mergeAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
|
||||||
|
list.setItemAnimator(new BetterItemAnimator());
|
||||||
|
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
|
||||||
|
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||||
|
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void proceedWithAuthOrSignup(Instance instance){
|
||||||
|
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||||
|
if(isSignup){
|
||||||
|
if(!instance.registrations){
|
||||||
|
new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(R.string.instance_signup_closed)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putParcelable("instance", Parcels.wrap(instance));
|
||||||
|
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||||
|
}else{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private String getEmojiForCategory(String category){
|
||||||
|
// return switch(category){
|
||||||
|
// case "all" -> "💬";
|
||||||
|
// case "academia" -> "📚";
|
||||||
|
// case "activism" -> "✊";
|
||||||
|
// case "food" -> "🍕";
|
||||||
|
// case "furry" -> "🦁";
|
||||||
|
// case "games" -> "🕹";
|
||||||
|
// case "general" -> "🐘";
|
||||||
|
// case "journalism" -> "📰";
|
||||||
|
// case "lgbt" -> "🏳️🌈";
|
||||||
|
// case "regional" -> "📍";
|
||||||
|
// case "art" -> "🎨";
|
||||||
|
// case "music" -> "🎼";
|
||||||
|
// case "tech" -> "📱";
|
||||||
|
// default -> "❓";
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
private int getEmojiForCategory(String category){
|
||||||
|
return switch(category){
|
||||||
|
case "all" -> R.drawable.ic_category_all;
|
||||||
|
case "academia" -> R.drawable.ic_category_academia;
|
||||||
|
case "activism" -> R.drawable.ic_category_activism;
|
||||||
|
case "food" -> R.drawable.ic_category_food;
|
||||||
|
case "furry" -> R.drawable.ic_category_furry;
|
||||||
|
case "games" -> R.drawable.ic_category_games;
|
||||||
|
case "general" -> R.drawable.ic_category_general;
|
||||||
|
case "journalism" -> R.drawable.ic_category_journalism;
|
||||||
|
case "lgbt" -> R.drawable.ic_category_lgbt;
|
||||||
|
case "regional" -> R.drawable.ic_category_regional;
|
||||||
|
case "art" -> R.drawable.ic_category_art;
|
||||||
|
case "music" -> R.drawable.ic_category_music;
|
||||||
|
case "tech" -> R.drawable.ic_category_tech;
|
||||||
|
default -> R.drawable.ic_category_unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getTitleForCategory(String category){
|
||||||
|
return switch(category){
|
||||||
|
case "all" -> R.string.category_all;
|
||||||
|
case "academia" -> R.string.category_academia;
|
||||||
|
case "activism" -> R.string.category_activism;
|
||||||
|
case "food" -> R.string.category_food;
|
||||||
|
case "furry" -> R.string.category_furry;
|
||||||
|
case "games" -> R.string.category_games;
|
||||||
|
case "general" -> R.string.category_general;
|
||||||
|
case "journalism" -> R.string.category_journalism;
|
||||||
|
case "lgbt" -> R.string.category_lgbt;
|
||||||
|
case "regional" -> R.string.category_regional;
|
||||||
|
case "art" -> R.string.category_art;
|
||||||
|
case "music" -> R.string.category_music;
|
||||||
|
case "tech" -> R.string.category_tech;
|
||||||
|
default -> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateFilteredList(){
|
||||||
|
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
|
||||||
|
filteredData.clear();
|
||||||
|
for(CatalogInstance instance:data){
|
||||||
|
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
|
||||||
|
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
|
||||||
|
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
|
||||||
|
filteredData.add(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiffUtil.calculateDiff(new DiffUtil.Callback(){
|
||||||
|
@Override
|
||||||
|
public int getOldListSize(){
|
||||||
|
return prevData.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getNewListSize(){
|
||||||
|
return filteredData.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
|
||||||
|
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
|
||||||
|
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||||
|
}
|
||||||
|
}).dispatchUpdatesTo(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
|
||||||
|
public InstancesAdapter(){
|
||||||
|
super(imgLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public InstanceCatalogSignupFragment.InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||||
|
return new InstanceCatalogSignupFragment.InstanceViewHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(InstanceCatalogSignupFragment.InstanceViewHolder holder, int position){
|
||||||
|
holder.bind(filteredData.get(position));
|
||||||
|
super.onBindViewHolder(holder, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount(){
|
||||||
|
return filteredData.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position){
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
|
||||||
|
private final TextView title, description, userCount, lang;
|
||||||
|
private final RadioButton radioButton;
|
||||||
|
|
||||||
|
public InstanceViewHolder(){
|
||||||
|
super(getActivity(), R.layout.item_instance_catalog, list);
|
||||||
|
title=findViewById(R.id.title);
|
||||||
|
description=findViewById(R.id.description);
|
||||||
|
userCount=findViewById(R.id.user_count);
|
||||||
|
lang=findViewById(R.id.lang);
|
||||||
|
radioButton=findViewById(R.id.radiobtn);
|
||||||
|
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
||||||
|
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
|
||||||
|
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBind(CatalogInstance item){
|
||||||
|
title.setText(item.normalizedDomain);
|
||||||
|
description.setText(item.description);
|
||||||
|
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
|
||||||
|
lang.setText(item.language.toUpperCase());
|
||||||
|
radioButton.setChecked(chosenInstance==item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(){
|
||||||
|
if(chosenInstance==item)
|
||||||
|
return;
|
||||||
|
if(chosenInstance!=null){
|
||||||
|
int idx=filteredData.indexOf(chosenInstance);
|
||||||
|
if(idx!=-1){
|
||||||
|
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
|
||||||
|
if(holder instanceof InstanceCatalogSignupFragment.InstanceViewHolder ivh){
|
||||||
|
ivh.radioButton.setChecked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
radioButton.setChecked(true);
|
||||||
|
if(chosenInstance==null)
|
||||||
|
nextButton.setEnabled(true);
|
||||||
|
chosenInstance=item;
|
||||||
|
loadInstanceInfo(chosenInstance.domain, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package org.joinmastodon.android.fragments.onboarding;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Outline;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewOutlineProvider;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
|
public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
|
||||||
|
private View headerView;
|
||||||
|
private boolean loadedAutocomplete;
|
||||||
|
private ImageButton clearBtn;
|
||||||
|
|
||||||
|
public InstanceChooserLoginFragment(){
|
||||||
|
super(R.layout.fragment_login, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
dataLoaded();
|
||||||
|
setTitle(R.string.login_title);
|
||||||
|
if(!loadedAutocomplete){
|
||||||
|
loadAutocompleteServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void proceedWithAuthOrSignup(Instance instance){
|
||||||
|
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateFilteredList(){
|
||||||
|
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
|
||||||
|
filteredData.clear();
|
||||||
|
if(currentSearchQuery.length()>0){
|
||||||
|
boolean foundExactMatch=false;
|
||||||
|
for(CatalogInstance inst:data){
|
||||||
|
if(inst.normalizedDomain.contains(currentSearchQuery)){
|
||||||
|
filteredData.add(inst);
|
||||||
|
if(inst.normalizedDomain.equals(currentSearchQuery))
|
||||||
|
foundExactMatch=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!foundExactMatch)
|
||||||
|
filteredData.add(0, fakeInstance);
|
||||||
|
}
|
||||||
|
UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals);
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
list.getChildAt(i).invalidateOutline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAutocompleteServers(){
|
||||||
|
loadedAutocomplete=true;
|
||||||
|
new GetCatalogInstances(null, null)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<CatalogInstance> result){
|
||||||
|
data.clear();
|
||||||
|
data.addAll(sortInstances(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.execNoAuth("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onUpdateToolbar(){
|
||||||
|
super.onUpdateToolbar();
|
||||||
|
Toolbar toolbar=getToolbar();
|
||||||
|
toolbar.setElevation(0);
|
||||||
|
toolbar.setBackground(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter getAdapter(){
|
||||||
|
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_login, list, false);
|
||||||
|
clearBtn=headerView.findViewById(R.id.search_clear);
|
||||||
|
searchEdit=headerView.findViewById(R.id.search_edit);
|
||||||
|
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
|
||||||
|
searchEdit.addTextChangedListener(new TextWatcher(){
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||||
|
searchEdit.removeCallbacks(searchDebouncer);
|
||||||
|
searchEdit.postDelayed(searchDebouncer, 300);
|
||||||
|
|
||||||
|
if(s.length()>0){
|
||||||
|
fakeInstance.domain=fakeInstance.normalizedDomain=s.toString();
|
||||||
|
fakeInstance.description=getString(R.string.loading_instance);
|
||||||
|
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
||||||
|
if(list.findViewHolderForAdapterPosition(1) instanceof InstanceViewHolder ivh){
|
||||||
|
ivh.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(filteredData.isEmpty()){
|
||||||
|
filteredData.add(fakeInstance);
|
||||||
|
adapter.notifyItemInserted(0);
|
||||||
|
}
|
||||||
|
clearBtn.setVisibility(View.VISIBLE);
|
||||||
|
}else{
|
||||||
|
clearBtn.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s){
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearBtn.setOnClickListener(v->searchEdit.setText(""));
|
||||||
|
|
||||||
|
mergeAdapter=new MergeRecyclerAdapter();
|
||||||
|
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||||
|
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
|
||||||
|
return mergeAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||||
|
|
||||||
|
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||||
|
@Override
|
||||||
|
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||||
|
if(parent.getChildViewHolder(view) instanceof InstanceViewHolder){
|
||||||
|
outRect.left=outRect.right=V.dp(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
((UsableRecyclerView)list).setDrawSelectorOnTop(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
|
||||||
|
public InstancesAdapter(){
|
||||||
|
super(imgLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||||
|
return new InstanceViewHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(InstanceViewHolder holder, int position){
|
||||||
|
holder.bind(filteredData.get(position));
|
||||||
|
super.onBindViewHolder(holder, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount(){
|
||||||
|
return filteredData.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position){
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
|
||||||
|
private final TextView title, description;
|
||||||
|
private final RadioButton radioButton;
|
||||||
|
|
||||||
|
public InstanceViewHolder(){
|
||||||
|
super(getActivity(), R.layout.item_instance_login, list);
|
||||||
|
title=findViewById(R.id.title);
|
||||||
|
description=findViewById(R.id.description);
|
||||||
|
radioButton=findViewById(R.id.radiobtn);
|
||||||
|
radioButton.setMinWidth(0);
|
||||||
|
radioButton.setMinHeight(0);
|
||||||
|
|
||||||
|
itemView.setOutlineProvider(new ViewOutlineProvider(){
|
||||||
|
@Override
|
||||||
|
public void getOutline(View view, Outline outline){
|
||||||
|
outline.setRoundRect(0, getAbsoluteAdapterPosition()==1 ? 0 : V.dp(-4), view.getWidth(), view.getHeight()+(getAbsoluteAdapterPosition()==filteredData.size() ? 0 : V.dp(4)), V.dp(4));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
itemView.setClipToOutline(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBind(CatalogInstance item){
|
||||||
|
title.setText(item.normalizedDomain);
|
||||||
|
description.setText(item.description);
|
||||||
|
radioButton.setChecked(chosenInstance==item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(){
|
||||||
|
if(chosenInstance==item)
|
||||||
|
return;
|
||||||
|
if(chosenInstance!=null){
|
||||||
|
int idx=filteredData.indexOf(chosenInstance);
|
||||||
|
if(idx!=-1){
|
||||||
|
boolean found=false;
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||||
|
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
|
||||||
|
ivh.radioButton.setChecked(false);
|
||||||
|
found=true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found)
|
||||||
|
adapter.notifyItemChanged(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
radioButton.setChecked(true);
|
||||||
|
if(chosenInstance==null)
|
||||||
|
nextButton.setEnabled(true);
|
||||||
|
chosenInstance=item;
|
||||||
|
loadInstanceInfo(chosenInstance.domain, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import android.widget.TextView;
|
|||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.model.Instance;
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
@@ -131,7 +132,10 @@ public class InstanceRulesFragment extends AppKitFragment{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(Instance.Rule item){
|
public void onBind(Instance.Rule item){
|
||||||
title.setText(item.text);
|
if(item.parsedText==null){
|
||||||
|
item.parsedText=HtmlParser.parseLinks(item.text);
|
||||||
|
}
|
||||||
|
title.setText(item.parsedText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import android.widget.Button;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||||
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
|
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
@@ -130,6 +132,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Relationship result){
|
public void onSuccess(Relationship result){
|
||||||
Nav.finish(ReportDoneFragment.this);
|
Nav.finish(ReportDoneFragment.this);
|
||||||
|
E.post(new RemoveAccountPostsEvent(accountID, reportAccount.id, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ public class Instance extends BaseModel{
|
|||||||
// non-standard field in some Mastodon forks
|
// non-standard field in some Mastodon forks
|
||||||
public int maxTootChars;
|
public int maxTootChars;
|
||||||
|
|
||||||
|
public V2 v2;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postprocess() throws ObjectValidationException{
|
public void postprocess() throws ObjectValidationException{
|
||||||
super.postprocess();
|
super.postprocess();
|
||||||
@@ -134,6 +136,8 @@ public class Instance extends BaseModel{
|
|||||||
public static class Rule{
|
public static class Rule{
|
||||||
public String id;
|
public String id;
|
||||||
public String text;
|
public String text;
|
||||||
|
|
||||||
|
public transient CharSequence parsedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
@@ -174,4 +178,19 @@ public class Instance extends BaseModel{
|
|||||||
public int minExpiration;
|
public int minExpiration;
|
||||||
public int maxExpiration;
|
public int maxExpiration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcel
|
||||||
|
public static class V2 extends BaseModel {
|
||||||
|
public V2.Configuration configuration;
|
||||||
|
|
||||||
|
@Parcel
|
||||||
|
public static class Configuration {
|
||||||
|
public TranslationConfiguration translation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcel
|
||||||
|
public static class TranslationConfiguration{
|
||||||
|
public boolean enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@AllFieldsAreRequired
|
||||||
|
public class Marker extends BaseModel{
|
||||||
|
public String lastReadId;
|
||||||
|
public long version;
|
||||||
|
public Instant updatedAt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString(){
|
||||||
|
return "Marker{"+
|
||||||
|
"lastReadId='"+lastReadId+'\''+
|
||||||
|
", version="+version+
|
||||||
|
", updatedAt="+updatedAt+
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public class Poll extends BaseModel{
|
|||||||
@RequiredField
|
@RequiredField
|
||||||
public String id;
|
public String id;
|
||||||
public Instant expiresAt;
|
public Instant expiresAt;
|
||||||
public boolean expired;
|
private boolean expired;
|
||||||
public boolean multiple;
|
public boolean multiple;
|
||||||
public int votersCount;
|
public int votersCount;
|
||||||
public boolean voted;
|
public boolean voted;
|
||||||
@@ -48,6 +48,10 @@ public class Poll extends BaseModel{
|
|||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isExpired(){
|
||||||
|
return expired || (expiresAt!=null && expiresAt.isBefore(Instant.now()));
|
||||||
|
}
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public static class Option{
|
public static class Option{
|
||||||
public String title;
|
public String title;
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ public class PushNotification extends BaseModel{
|
|||||||
@SerializedName("follow")
|
@SerializedName("follow")
|
||||||
FOLLOW(R.string.notification_type_follow),
|
FOLLOW(R.string.notification_type_follow),
|
||||||
@SerializedName("poll")
|
@SerializedName("poll")
|
||||||
POLL(R.string.notification_type_poll);
|
POLL(R.string.notification_type_poll),
|
||||||
|
@SerializedName("status")
|
||||||
|
STATUS(R.string.sk_notification_type_status);
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
public final int localizedName;
|
public final int localizedName;
|
||||||
|
|||||||
@@ -43,10 +43,11 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
|||||||
public boolean reblog;
|
public boolean reblog;
|
||||||
public boolean mention;
|
public boolean mention;
|
||||||
public boolean poll;
|
public boolean poll;
|
||||||
|
public boolean status;
|
||||||
|
|
||||||
public static Alerts ofAll(){
|
public static Alerts ofAll(){
|
||||||
Alerts alerts=new Alerts();
|
Alerts alerts=new Alerts();
|
||||||
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=true;
|
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=alerts.status=true;
|
||||||
return alerts;
|
return alerts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
|||||||
", reblog="+reblog+
|
", reblog="+reblog+
|
||||||
", mention="+mention+
|
", mention="+mention+
|
||||||
", poll="+poll+
|
", poll="+poll+
|
||||||
|
", status="+status+
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
|||||||
repliesCount=ev.replies;
|
repliesCount=ev.replies;
|
||||||
favourited=ev.favorited;
|
favourited=ev.favorited;
|
||||||
reblogged=ev.reblogged;
|
reblogged=ev.reblogged;
|
||||||
|
bookmarked=ev.bookmarked;
|
||||||
pinned=ev.pinned;
|
pinned=ev.pinned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
public class TranslatedStatus extends BaseModel {
|
||||||
|
public String content;
|
||||||
|
public String detectedSourceLanguage;
|
||||||
|
public String provider;
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
@@ -154,11 +155,16 @@ public class ComposeAutocompleteViewController{
|
|||||||
}else if(mode==Mode.EMOJIS){
|
}else if(mode==Mode.EMOJIS){
|
||||||
String _text=text.substring(1); // remove ':'
|
String _text=text.substring(1); // remove ':'
|
||||||
List<WrappedEmoji> oldList=emojis;
|
List<WrappedEmoji> oldList=emojis;
|
||||||
emojis=AccountSessionManager.getInstance()
|
List<Emoji> allEmojis = AccountSessionManager.getInstance()
|
||||||
.getCustomEmojis(AccountSessionManager.getInstance().getAccount(accountID).domain)
|
.getCustomEmojis(AccountSessionManager.getInstance().getAccount(accountID).domain)
|
||||||
.stream()
|
.stream()
|
||||||
.flatMap(ec->ec.emojis.stream())
|
.flatMap(ec->ec.emojis.stream())
|
||||||
.filter(e->e.visibleInPicker && e.shortcode.startsWith(_text))
|
.filter(e->e.visibleInPicker)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<Emoji> startsWithSearch = allEmojis.stream().filter(e -> e.shortcode.toLowerCase().startsWith(_text.toLowerCase())).collect(Collectors.toList());
|
||||||
|
emojis=Stream.concat(startsWithSearch.stream(), allEmojis.stream()
|
||||||
|
.filter(e -> !startsWithSearch.contains(e))
|
||||||
|
.filter(e -> e.shortcode.toLowerCase().contains(_text.toLowerCase())))
|
||||||
.map(WrappedEmoji::new)
|
.map(WrappedEmoji::new)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
|
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class ImageDescriptionSheet extends BottomSheet{
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextView heading=new TextView(activity);
|
TextView heading=new TextView(activity);
|
||||||
heading.setText(R.string.image_description);
|
heading.setText(R.string.sk_image_description);
|
||||||
heading.setAllCaps(true);
|
heading.setAllCaps(true);
|
||||||
heading.setTypeface(null, Typeface.BOLD);
|
heading.setTypeface(null, Typeface.BOLD);
|
||||||
heading.setPadding(0, V.dp(24), 0, V.dp(8));
|
heading.setPadding(0, V.dp(24), 0, V.dp(8));
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
private void bindButton(TextView btn, long count){
|
private void bindButton(TextView btn, long count){
|
||||||
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
|
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
|
||||||
btn.setText(DecimalFormat.getIntegerInstance().format(count));
|
btn.setText(UiUtils.abbreviateNumber(count));
|
||||||
btn.setCompoundDrawablePadding(V.dp(8));
|
btn.setCompoundDrawablePadding(V.dp(8));
|
||||||
}else{
|
}else{
|
||||||
btn.setText("");
|
btn.setText("");
|
||||||
@@ -147,7 +147,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
if(id==R.id.favorite_btn)
|
if(id==R.id.favorite_btn)
|
||||||
return R.string.button_favorite;
|
return R.string.button_favorite;
|
||||||
if(id==R.id.bookmark_btn)
|
if(id==R.id.bookmark_btn)
|
||||||
return R.string.button_bookmark;
|
return R.string.add_bookmark;
|
||||||
if(id==R.id.share_btn)
|
if(id==R.id.share_btn)
|
||||||
return R.string.button_share;
|
return R.string.button_share;
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.app.ProgressDialog;
|
|||||||
import android.graphics.Outline;
|
import android.graphics.Outline;
|
||||||
import android.graphics.drawable.Animatable;
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@@ -209,6 +210,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
});
|
});
|
||||||
}else if(id==R.id.block_domain){
|
}else if(id==R.id.block_domain){
|
||||||
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
|
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
|
||||||
|
}else if(id==R.id.bookmark){
|
||||||
|
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -226,6 +229,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
if(item.hasVisibilityToggle){
|
if(item.hasVisibilityToggle){
|
||||||
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
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));
|
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
|
||||||
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||||
|
visibility.setTooltipText(visibility.getContentDescription());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
||||||
if(TextUtils.isEmpty(item.extraText)){
|
if(TextUtils.isEmpty(item.extraText)){
|
||||||
@@ -306,6 +312,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
MenuItem block=menu.findItem(R.id.block);
|
MenuItem block=menu.findItem(R.id.block);
|
||||||
MenuItem report=menu.findItem(R.id.report);
|
MenuItem report=menu.findItem(R.id.report);
|
||||||
MenuItem follow=menu.findItem(R.id.follow);
|
MenuItem follow=menu.findItem(R.id.follow);
|
||||||
|
MenuItem bookmark=menu.findItem(R.id.bookmark);
|
||||||
|
bookmark.setVisible(false);
|
||||||
|
/* disabled in megalodon: add/remove bookmark is already available through status footer
|
||||||
|
if(item.status!=null){
|
||||||
|
bookmark.setVisible(true);
|
||||||
|
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
|
||||||
|
}else{
|
||||||
|
bookmark.setVisible(false);
|
||||||
|
}
|
||||||
|
*/
|
||||||
if(isOwnPost){
|
if(isOwnPost){
|
||||||
mute.setVisible(false);
|
mute.setVisible(false);
|
||||||
block.setVisible(false);
|
block.setVisible(false);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.model.Poll;
|
import org.joinmastodon.android.model.Poll;
|
||||||
@@ -38,13 +39,13 @@ public class PollFooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
@Override
|
@Override
|
||||||
public void onBind(PollFooterStatusDisplayItem item){
|
public void onBind(PollFooterStatusDisplayItem item){
|
||||||
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_voters, item.poll.votersCount, item.poll.votersCount);
|
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_voters, item.poll.votersCount, item.poll.votersCount);
|
||||||
if(item.poll.expiresAt!=null && !item.poll.expired){
|
if(item.poll.expiresAt!=null && !item.poll.isExpired()){
|
||||||
text+=" · "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
|
text+=" · "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
|
||||||
}else if(item.poll.expired){
|
}else if(item.poll.isExpired()){
|
||||||
text+=" · "+item.parentFragment.getString(R.string.poll_closed);
|
text+=" · "+item.parentFragment.getString(R.string.poll_closed);
|
||||||
}
|
}
|
||||||
this.text.setText(text);
|
this.text.setText(text);
|
||||||
button.setVisibility(item.poll.expired || item.poll.voted || !item.poll.multiple ? View.GONE : View.VISIBLE);
|
button.setVisibility(item.poll.isExpired() || item.poll.voted || (!item.poll.multiple && !GlobalUserPreferences.voteButtonForSingleChoice) ? View.GONE : View.VISIBLE);
|
||||||
button.setEnabled(item.poll.selectedOptions!=null && !item.poll.selectedOptions.isEmpty());
|
button.setEnabled(item.poll.selectedOptions!=null && !item.poll.selectedOptions.isEmpty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.graphics.drawable.Animatable;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
@@ -33,7 +34,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
|||||||
this.poll=poll;
|
this.poll=poll;
|
||||||
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
|
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
|
||||||
emojiHelper.setText(text);
|
emojiHelper.setText(text);
|
||||||
showResults=poll.expired || poll.voted;
|
showResults=poll.isExpired() || poll.voted;
|
||||||
if(showResults && option.votesCount!=null && poll.votersCount>0){
|
if(showResults && option.votesCount!=null && poll.votersCount>0){
|
||||||
votesFraction=(float)option.votesCount/(float)poll.votersCount;
|
votesFraction=(float)option.votesCount/(float)poll.votersCount;
|
||||||
int mostVotedCount=0;
|
int mostVotedCount=0;
|
||||||
@@ -60,7 +61,8 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<PollOptionStatusDisplayItem> implements ImageLoaderViewHolder{
|
public static class Holder extends StatusDisplayItem.Holder<PollOptionStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||||
private final TextView text, percent;
|
private final TextView text, percent;
|
||||||
private final View icon, button;
|
private final View button;
|
||||||
|
private final ImageView icon;
|
||||||
private final Drawable progressBg;
|
private final Drawable progressBg;
|
||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
@@ -76,13 +78,17 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
|||||||
@Override
|
@Override
|
||||||
public void onBind(PollOptionStatusDisplayItem item){
|
public void onBind(PollOptionStatusDisplayItem item){
|
||||||
text.setText(item.text);
|
text.setText(item.text);
|
||||||
icon.setVisibility(item.showResults ? View.GONE : View.VISIBLE);
|
|
||||||
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
|
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
|
||||||
itemView.setClickable(!item.showResults);
|
itemView.setClickable(!item.showResults);
|
||||||
|
icon.setImageDrawable(itemView.getContext().getDrawable(item.poll.multiple ?
|
||||||
|
item.showResults ? R.drawable.ic_poll_checkbox_regular_selector : R.drawable.ic_poll_checkbox_filled_selector :
|
||||||
|
item.showResults ? R.drawable.ic_poll_option_button : R.drawable.ic_fluent_radio_button_24_selector
|
||||||
|
));
|
||||||
if(item.showResults){
|
if(item.showResults){
|
||||||
progressBg.setLevel(Math.round(10000f*item.votesFraction));
|
progressBg.setLevel(Math.round(10000f*item.votesFraction));
|
||||||
button.setBackground(progressBg);
|
button.setBackground(progressBg);
|
||||||
itemView.setSelected(item.isMostVoted);
|
itemView.setSelected(item.isMostVoted);
|
||||||
|
icon.setSelected(item.poll.ownVotes.contains(item.poll.options.indexOf(item.option)));
|
||||||
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
|
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
|
||||||
}else{
|
}else{
|
||||||
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
|
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package org.joinmastodon.android.ui.displayitems;
|
package org.joinmastodon.android.ui.displayitems;
|
||||||
|
|
||||||
|
import static org.joinmastodon.android.MastodonApp.context;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -38,6 +41,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
|||||||
emojiHelper.setText(ssb);
|
emojiHelper.setText(ssb);
|
||||||
this.icon=icon;
|
this.icon=icon;
|
||||||
this.handleClick=handleClick;
|
this.handleClick=handleClick;
|
||||||
|
TypedValue outValue = new TypedValue();
|
||||||
|
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -67,6 +72,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
|||||||
text.setText(item.text);
|
text.setText(item.text);
|
||||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0);
|
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0);
|
||||||
if(item.handleClick!=null) text.setOnClickListener(item.handleClick);
|
if(item.handleClick!=null) text.setOnClickListener(item.handleClick);
|
||||||
|
text.setEnabled(!item.inset);
|
||||||
|
text.setClickable(!item.inset);
|
||||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
|
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,25 @@ import android.util.TypedValue;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||||
|
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.BaseStatusListFragment;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
||||||
|
import org.joinmastodon.android.model.StatusPrivacy;
|
||||||
|
import org.joinmastodon.android.model.TranslatedStatus;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||||
import me.grishka.appkit.imageloader.MovieDrawable;
|
import me.grishka.appkit.imageloader.MovieDrawable;
|
||||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||||
@@ -30,6 +39,12 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
private CharSequence parsedSpoilerText;
|
private CharSequence parsedSpoilerText;
|
||||||
public boolean textSelectable;
|
public boolean textSelectable;
|
||||||
public final Status status;
|
public final Status status;
|
||||||
|
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){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
@@ -41,6 +56,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
spoilerEmojiHelper=new CustomEmojiHelper();
|
spoilerEmojiHelper=new CustomEmojiHelper();
|
||||||
spoilerEmojiHelper.setText(parsedSpoilerText);
|
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
|
@Override
|
||||||
@@ -65,9 +83,10 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||||
private final LinkedTextView text;
|
private final LinkedTextView text;
|
||||||
private final LinearLayout spoilerHeader;
|
private final LinearLayout spoilerHeader;
|
||||||
private final TextView spoilerTitle, spoilerTitleInline;
|
private final TextView spoilerTitle, spoilerTitleInline, translateInfo;
|
||||||
private final View spoilerOverlay, borderTop, borderBottom;
|
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress;
|
||||||
private final Drawable backgroundColor, borderColor;
|
private final Drawable backgroundColor, borderColor;
|
||||||
|
private final Button translateButton;
|
||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
super(activity, R.layout.display_item_text, parent);
|
super(activity, R.layout.display_item_text, parent);
|
||||||
@@ -78,6 +97,11 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
spoilerOverlay=findViewById(R.id.spoiler_overlay);
|
spoilerOverlay=findViewById(R.id.spoiler_overlay);
|
||||||
borderTop=findViewById(R.id.border_top);
|
borderTop=findViewById(R.id.border_top);
|
||||||
borderBottom=findViewById(R.id.border_bottom);
|
borderBottom=findViewById(R.id.border_bottom);
|
||||||
|
textWrap=findViewById(R.id.text_wrap);
|
||||||
|
translateWrap=findViewById(R.id.translate_wrap);
|
||||||
|
translateButton=findViewById(R.id.translate_btn);
|
||||||
|
translateInfo=findViewById(R.id.translate_info);
|
||||||
|
translateProgress=findViewById(R.id.translate_progress);
|
||||||
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||||
|
|
||||||
TypedValue outValue=new TypedValue();
|
TypedValue outValue=new TypedValue();
|
||||||
@@ -91,7 +115,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(TextStatusDisplayItem item){
|
public void onBind(TextStatusDisplayItem item){
|
||||||
text.setText(item.text);
|
text.setText(item.translated
|
||||||
|
? HtmlParser.parse(item.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID())
|
||||||
|
: item.text);
|
||||||
text.setTextIsSelectable(item.textSelectable);
|
text.setTextIsSelectable(item.textSelectable);
|
||||||
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
|
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
|
||||||
text.setInvalidateOnEveryFrame(false);
|
text.setInvalidateOnEveryFrame(false);
|
||||||
@@ -105,20 +131,53 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
if(item.status.spoilerRevealed){
|
if(item.status.spoilerRevealed){
|
||||||
spoilerOverlay.setVisibility(View.GONE);
|
spoilerOverlay.setVisibility(View.GONE);
|
||||||
spoilerHeader.setVisibility(View.VISIBLE);
|
spoilerHeader.setVisibility(View.VISIBLE);
|
||||||
text.setVisibility(View.VISIBLE);
|
textWrap.setVisibility(View.VISIBLE);
|
||||||
itemView.setClickable(false);
|
itemView.setClickable(false);
|
||||||
}else{
|
}else{
|
||||||
spoilerOverlay.setVisibility(View.VISIBLE);
|
spoilerOverlay.setVisibility(View.VISIBLE);
|
||||||
spoilerHeader.setVisibility(View.GONE);
|
spoilerHeader.setVisibility(View.GONE);
|
||||||
text.setVisibility(View.GONE);
|
textWrap.setVisibility(View.GONE);
|
||||||
itemView.setClickable(true);
|
itemView.setClickable(true);
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
spoilerOverlay.setVisibility(View.GONE);
|
spoilerOverlay.setVisibility(View.GONE);
|
||||||
spoilerHeader.setVisibility(View.GONE);
|
spoilerHeader.setVisibility(View.GONE);
|
||||||
text.setVisibility(View.VISIBLE);
|
textWrap.setVisibility(View.VISIBLE);
|
||||||
itemView.setClickable(false);
|
itemView.setClickable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
translateWrap.setVisibility(item.textSelectable && item.translateEnabled &&
|
||||||
|
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
|
||||||
|
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))
|
||||||
|
? View.VISIBLE : View.GONE);
|
||||||
|
translateButton.setText(item.translated ? R.string.sk_translate_show_original : R.string.sk_translate_post);
|
||||||
|
translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, item.translation.provider) : "");
|
||||||
|
translateButton.setOnClickListener(v->{
|
||||||
|
if (item.translation == null) {
|
||||||
|
translateProgress.setVisibility(View.VISIBLE);
|
||||||
|
translateButton.setClickable(false);
|
||||||
|
new TranslateStatus(item.status.id).setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(TranslatedStatus translatedStatus) {
|
||||||
|
item.translation = translatedStatus;
|
||||||
|
item.translated = true;
|
||||||
|
translateProgress.setVisibility(View.GONE);
|
||||||
|
translateButton.setClickable(true);
|
||||||
|
rebind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
translateProgress.setVisibility(View.GONE);
|
||||||
|
translateButton.setClickable(true);
|
||||||
|
error.showToast(itemView.getContext());
|
||||||
|
}
|
||||||
|
}).exec(item.parentFragment.getAccountID());
|
||||||
|
} else {
|
||||||
|
item.translated = !item.translated;
|
||||||
|
rebind();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
|||||||
toolbar=uiOverlay.findViewById(R.id.toolbar);
|
toolbar=uiOverlay.findViewById(R.id.toolbar);
|
||||||
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
|
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
|
||||||
imageDescriptionButton = toolbar.getMenu()
|
imageDescriptionButton = toolbar.getMenu()
|
||||||
.add(R.string.image_description)
|
.add(R.string.sk_image_description)
|
||||||
.setIcon(R.drawable.ic_fluent_image_alt_text_24_regular)
|
.setIcon(R.drawable.ic_fluent_image_alt_text_24_regular)
|
||||||
.setVisible(attachments.get(pager.getCurrentItem()).description != null
|
.setVisible(attachments.get(pager.getCurrentItem()).description != null
|
||||||
&& !attachments.get(pager.getCurrentItem()).description.isEmpty())
|
&& !attachments.get(pager.getCurrentItem()).description.isEmpty())
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
package org.joinmastodon.android.ui.text;
|
package org.joinmastodon.android.ui.text;
|
||||||
|
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.fonts.FontFamily;
|
||||||
|
import android.graphics.fonts.FontStyle;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
|
import android.text.style.BulletSpan;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
import android.text.style.RelativeSizeSpan;
|
||||||
|
import android.text.style.StrikethroughSpan;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
|
import android.text.style.SubscriptSpan;
|
||||||
|
import android.text.style.SuperscriptSpan;
|
||||||
|
import android.text.style.TypefaceSpan;
|
||||||
|
import android.text.style.UnderlineSpan;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.twitter.twittertext.Regex;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.MastodonApp;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
import org.joinmastodon.android.model.Hashtag;
|
import org.joinmastodon.android.model.Hashtag;
|
||||||
import org.joinmastodon.android.model.Mention;
|
import org.joinmastodon.android.model.Mention;
|
||||||
@@ -12,11 +32,11 @@ import org.jsoup.Jsoup;
|
|||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.jsoup.nodes.Node;
|
import org.jsoup.nodes.Node;
|
||||||
import org.jsoup.nodes.TextNode;
|
import org.jsoup.nodes.TextNode;
|
||||||
import org.jsoup.safety.Cleaner;
|
|
||||||
import org.jsoup.safety.Safelist;
|
import org.jsoup.safety.Safelist;
|
||||||
import org.jsoup.select.NodeVisitor;
|
import org.jsoup.select.NodeVisitor;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -26,8 +46,25 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class HtmlParser{
|
public class HtmlParser{
|
||||||
private static final String TAG="HtmlParser";
|
private static final String TAG="HtmlParser";
|
||||||
|
private static final String VALID_URL_PATTERN_STRING =
|
||||||
|
"(" + // $1 total match
|
||||||
|
"(" + Regex.URL_VALID_PRECEDING_CHARS + ")" + // $2 Preceding character
|
||||||
|
"(" + // $3 URL
|
||||||
|
"(https?://)" + // $4 Protocol (optional)
|
||||||
|
"(" + Regex.URL_VALID_DOMAIN + ")" + // $5 Domain(s)
|
||||||
|
"(?::(" + Regex.URL_VALID_PORT_NUMBER + "))?" + // $6 Port number (optional)
|
||||||
|
"(/" +
|
||||||
|
Regex.URL_VALID_PATH + "*+" +
|
||||||
|
")?" + // $7 URL Path and anchor
|
||||||
|
"(\\?" + Regex.URL_VALID_URL_QUERY_CHARS + "*" + // $8 Query String
|
||||||
|
Regex.URL_VALID_URL_QUERY_ENDING_CHARS + ")?" +
|
||||||
|
")" +
|
||||||
|
")";
|
||||||
|
public static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
|
||||||
private static Pattern EMOJI_CODE_PATTERN=Pattern.compile(":([\\w]+):");
|
private static Pattern EMOJI_CODE_PATTERN=Pattern.compile(":([\\w]+):");
|
||||||
|
|
||||||
private HtmlParser(){}
|
private HtmlParser(){}
|
||||||
@@ -49,11 +86,17 @@ public class HtmlParser{
|
|||||||
public Object span;
|
public Object span;
|
||||||
public int start;
|
public int start;
|
||||||
public Element element;
|
public Element element;
|
||||||
|
public boolean more;
|
||||||
|
|
||||||
public SpanInfo(Object span, int start, Element element){
|
public SpanInfo(Object span, int start, Element element){
|
||||||
|
this(span, start, element, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpanInfo(Object span, int start, Element element, boolean more){
|
||||||
this.span=span;
|
this.span=span;
|
||||||
this.start=start;
|
this.start=start;
|
||||||
this.element=element;
|
this.element=element;
|
||||||
|
this.more=more;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,24 +144,59 @@ public class HtmlParser{
|
|||||||
openSpans.add(new SpanInfo(new InvisibleSpan(), ssb.length(), el));
|
openSpans.add(new SpanInfo(new InvisibleSpan(), ssb.length(), el));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "li" -> openSpans.add(new SpanInfo(new BulletSpan(V.dp(8)), ssb.length(), el));
|
||||||
|
case "em", "i" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.ITALIC), ssb.length(), el));
|
||||||
|
case "h1", "h2", "h3", "h4", "h5", "h6" -> {
|
||||||
|
// increase line height above heading (multiplying the margin)
|
||||||
|
if (node.previousSibling()!=null) ssb.setSpan(new RelativeSizeSpan(2), ssb.length() - 1, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
if (!node.nodeName().equals("h1")) {
|
||||||
|
openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
|
||||||
|
}
|
||||||
|
openSpans.add(new SpanInfo(new RelativeSizeSpan(switch(node.nodeName()) {
|
||||||
|
case "h1" -> 1.5f;
|
||||||
|
case "h2" -> 1.25f;
|
||||||
|
case "h3" -> 1.125f;
|
||||||
|
default -> 1;
|
||||||
|
}), ssb.length(), el, !node.nodeName().equals("h1")));
|
||||||
|
}
|
||||||
|
case "strong", "b" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
|
||||||
|
case "u" -> openSpans.add(new SpanInfo(new UnderlineSpan(), ssb.length(), el));
|
||||||
|
case "s", "del" -> openSpans.add(new SpanInfo(new StrikethroughSpan(), ssb.length(), el));
|
||||||
|
case "sub", "sup" -> {
|
||||||
|
openSpans.add(new SpanInfo(node.nodeName().equals("sub") ? new SubscriptSpan() : new SuperscriptSpan(), ssb.length(), el));
|
||||||
|
openSpans.add(new SpanInfo(new RelativeSizeSpan(0.8f), ssb.length(), el, true));
|
||||||
|
}
|
||||||
|
case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el));
|
||||||
|
case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final static List<String> blockElements = Arrays.asList("p", "ul", "ol", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void tail(@NonNull Node node, int depth){
|
public void tail(@NonNull Node node, int depth){
|
||||||
if(node instanceof Element el){
|
if(node instanceof Element el){
|
||||||
|
processOpenSpan(el);
|
||||||
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
|
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
|
||||||
ssb.append("…", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
ssb.append("…", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}else if("p".equals(el.nodeName())){
|
}else if(blockElements.contains(el.nodeName()) && node.nextSibling()!=null){
|
||||||
if(node.nextSibling()!=null)
|
ssb.append("\n"); // line end
|
||||||
ssb.append("\n\n");
|
ssb.append("\n", new RelativeSizeSpan(0.75f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block
|
||||||
}else if(!openSpans.isEmpty()){
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processOpenSpan(Element el) {
|
||||||
|
if(!openSpans.isEmpty()){
|
||||||
SpanInfo si=openSpans.get(openSpans.size()-1);
|
SpanInfo si=openSpans.get(openSpans.size()-1);
|
||||||
if(si.element==el){
|
if(si.element==el){
|
||||||
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
openSpans.remove(openSpans.size()-1);
|
openSpans.remove(openSpans.size()-1);
|
||||||
|
if(si.more) processOpenSpan(el);
|
||||||
}
|
}
|
||||||
|
if("li".equals(el.nodeName()) && el.nextSibling()!=null) {
|
||||||
|
ssb.append('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,4 +250,18 @@ public class HtmlParser{
|
|||||||
public static String strip(String html){
|
public static String strip(String html){
|
||||||
return Jsoup.clean(html, Safelist.none());
|
return Jsoup.clean(html, Safelist.none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CharSequence parseLinks(String text){
|
||||||
|
Matcher matcher=URL_PATTERN.matcher(text);
|
||||||
|
if(!matcher.find()) // Return the original string if there are no URLs
|
||||||
|
return text;
|
||||||
|
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
|
||||||
|
do{
|
||||||
|
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);
|
||||||
|
}while(matcher.find()); // Find more URLs
|
||||||
|
return ssb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class DiscoverInfoBannerHelper{
|
|||||||
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
|
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
|
||||||
case TRENDING_LINKS -> R.string.trending_links_info_banner;
|
case TRENDING_LINKS -> R.string.trending_links_info_banner;
|
||||||
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
||||||
case FEDERATED_TIMELINE -> R.string.federated_timeline_info_banner;
|
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import android.os.Bundle;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@@ -48,6 +49,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
|||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
||||||
import org.joinmastodon.android.events.NotificationDeletedEvent;
|
import org.joinmastodon.android.events.NotificationDeletedEvent;
|
||||||
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
@@ -345,6 +347,9 @@ public class UiUtils{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Relationship result){
|
public void onSuccess(Relationship result){
|
||||||
resultCallback.accept(result);
|
resultCallback.accept(result);
|
||||||
|
if(!currentlyBlocked){
|
||||||
|
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -387,6 +392,9 @@ public class UiUtils{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Relationship result){
|
public void onSuccess(Relationship result){
|
||||||
resultCallback.accept(result);
|
resultCallback.accept(result);
|
||||||
|
if(!currentlyMuted){
|
||||||
|
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -403,7 +411,7 @@ public class UiUtils{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){
|
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){
|
||||||
showConfirmationAlert(activity, forRedraft ? R.string.confirm_delete_and_redraft_title : R.string.confirm_delete_title, forRedraft ? R.string.confirm_delete_and_redraft : R.string.confirm_delete, forRedraft ? R.string.delete_and_redraft : R.string.delete, ()->{
|
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)
|
new DeleteStatus(status.id)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
@@ -425,9 +433,9 @@ public class UiUtils{
|
|||||||
|
|
||||||
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
|
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
|
||||||
showConfirmationAlert(activity,
|
showConfirmationAlert(activity,
|
||||||
pinned ? R.string.confirm_pin_post_title : R.string.confirm_unpin_post_title,
|
pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title,
|
||||||
pinned ? R.string.confirm_pin_post : R.string.confirm_unpin_post,
|
pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post,
|
||||||
pinned ? R.string.pin_post : R.string.unpin_post,
|
pinned ? R.string.sk_pin_post : R.string.sk_unpin_post,
|
||||||
()->{
|
()->{
|
||||||
new SetStatusPinned(status.id, pinned)
|
new SetStatusPinned(status.id, pinned)
|
||||||
.setCallback(new Callback<>() {
|
.setCallback(new Callback<>() {
|
||||||
@@ -444,7 +452,7 @@ public class UiUtils{
|
|||||||
error.showToast(activity);
|
error.showToast(activity);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress(activity, pinned ? R.string.pinning : R.string.unpinning, false)
|
.wrapProgress(activity, pinned ? R.string.sk_pinning : R.string.sk_unpinning, false)
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -500,7 +508,7 @@ public class UiUtils{
|
|||||||
public void onSuccess(Relationship result) {
|
public void onSuccess(Relationship result) {
|
||||||
resultCallback.accept(result);
|
resultCallback.accept(result);
|
||||||
progressCallback.accept(false);
|
progressCallback.accept(false);
|
||||||
Toast.makeText(activity, activity.getString(result.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username), Toast.LENGTH_SHORT).show();
|
Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -524,6 +532,9 @@ public class UiUtils{
|
|||||||
public void onSuccess(Relationship result){
|
public void onSuccess(Relationship result){
|
||||||
resultCallback.accept(result);
|
resultCallback.accept(result);
|
||||||
progressCallback.accept(false);
|
progressCallback.accept(false);
|
||||||
|
if(!result.following){
|
||||||
|
E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -645,13 +656,71 @@ public class UiUtils{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void setUserPreferredTheme(Context context){
|
public static void setUserPreferredTheme(Context context){
|
||||||
|
// boolean isDarkTheme = isDarkTheme();
|
||||||
|
switch(GlobalUserPreferences.color){
|
||||||
|
case PINK:
|
||||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||||
case AUTO -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
|
case AUTO ->
|
||||||
case LIGHT -> R.style.Theme_Mastodon_Light;
|
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
|
||||||
case DARK -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
|
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 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
public static boolean isDarkTheme(){
|
public static boolean isDarkTheme(){
|
||||||
if(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO)
|
if(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO)
|
||||||
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
|
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
|
||||||
@@ -660,8 +729,7 @@ public class UiUtils{
|
|||||||
|
|
||||||
public static void openURL(Context context, @Nullable String accountID, String url){
|
public static void openURL(Context context, @Nullable String accountID, String url){
|
||||||
Uri uri=Uri.parse(url);
|
Uri uri=Uri.parse(url);
|
||||||
String accountDomain=accountID != null ? AccountSessionManager.getInstance().getAccount(accountID).domain : null;
|
if(accountID!=null && "https".equals(uri.getScheme()) && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
|
||||||
if(accountDomain!=null && "https".equals(uri.getScheme()) && accountDomain.equalsIgnoreCase(uri.getAuthority())){
|
|
||||||
List<String> path=uri.getPathSegments();
|
List<String> path=uri.getPathSegments();
|
||||||
// Match URLs like https://mastodon.social/@Gargron/108132679274083591
|
// 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]+$")){
|
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$")){
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.net.Uri;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.DragEvent;
|
import android.view.DragEvent;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
@@ -22,7 +21,6 @@ import androidx.annotation.RequiresApi;
|
|||||||
|
|
||||||
public class ComposeEditText extends EditText{
|
public class ComposeEditText extends EditText{
|
||||||
private SelectionListener selectionListener;
|
private SelectionListener selectionListener;
|
||||||
private MediaAcceptingInputConnection inputConnectionWrapper=new MediaAcceptingInputConnection();
|
|
||||||
|
|
||||||
public ComposeEditText(Context context){
|
public ComposeEditText(Context context){
|
||||||
super(context);
|
super(context);
|
||||||
@@ -54,11 +52,10 @@ public class ComposeEditText extends EditText{
|
|||||||
// Support receiving images from keyboards
|
// Support receiving images from keyboards
|
||||||
@Override
|
@Override
|
||||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs){
|
public InputConnection onCreateInputConnection(EditorInfo outAttrs){
|
||||||
final var ic = super.onCreateInputConnection(outAttrs);
|
final InputConnection ic=super.onCreateInputConnection(outAttrs);
|
||||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N_MR1){
|
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N_MR1){
|
||||||
outAttrs.contentMimeTypes=selectionListener.onGetAllowedMediaMimeTypes();
|
outAttrs.contentMimeTypes=selectionListener.onGetAllowedMediaMimeTypes();
|
||||||
inputConnectionWrapper.setTarget(ic);
|
return new MediaAcceptingInputConnection(ic);
|
||||||
return inputConnectionWrapper;
|
|
||||||
}
|
}
|
||||||
return ic;
|
return ic;
|
||||||
}
|
}
|
||||||
@@ -106,8 +103,8 @@ public class ComposeEditText extends EditText{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class MediaAcceptingInputConnection extends InputConnectionWrapper{
|
private class MediaAcceptingInputConnection extends InputConnectionWrapper{
|
||||||
public MediaAcceptingInputConnection(){
|
public MediaAcceptingInputConnection(InputConnection conn){
|
||||||
super(null, true);
|
super(conn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api=Build.VERSION_CODES.N_MR1)
|
@RequiresApi(api=Build.VERSION_CODES.N_MR1)
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package org.joinmastodon.android.ui.views;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.AnimatorSet;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||||
|
|
||||||
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
|
public class FloatingHintEditTextLayout extends FrameLayout{
|
||||||
|
private EditText edit;
|
||||||
|
private TextView label;
|
||||||
|
private int labelTextSize;
|
||||||
|
private int offsetY;
|
||||||
|
private boolean hintVisible;
|
||||||
|
private Animator currentAnim;
|
||||||
|
|
||||||
|
public FloatingHintEditTextLayout(Context context){
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FloatingHintEditTextLayout(Context context, AttributeSet attrs){
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FloatingHintEditTextLayout(Context context, AttributeSet attrs, int defStyle){
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
if(isInEditMode())
|
||||||
|
V.setApplicationContext(context);
|
||||||
|
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FloatingHintEditTextLayout);
|
||||||
|
labelTextSize=ta.getDimensionPixelSize(R.styleable.FloatingHintEditTextLayout_android_labelTextSize, V.dp(12));
|
||||||
|
offsetY=ta.getDimensionPixelOffset(R.styleable.FloatingHintEditTextLayout_editTextOffsetY, 0);
|
||||||
|
ta.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFinishInflate(){
|
||||||
|
super.onFinishInflate();
|
||||||
|
if(getChildCount()>0 && getChildAt(0) instanceof EditText et){
|
||||||
|
edit=et;
|
||||||
|
}else{
|
||||||
|
throw new IllegalStateException("First child must be an EditText");
|
||||||
|
}
|
||||||
|
|
||||||
|
label=new TextView(getContext());
|
||||||
|
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize);
|
||||||
|
label.setTextColor(edit.getHintTextColors());
|
||||||
|
label.setText(edit.getHint());
|
||||||
|
label.setSingleLine();
|
||||||
|
label.setPivotX(0f);
|
||||||
|
label.setPivotY(0f);
|
||||||
|
LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP);
|
||||||
|
lp.setMarginStart(edit.getPaddingStart());
|
||||||
|
addView(label, lp);
|
||||||
|
|
||||||
|
hintVisible=edit.getText().length()==0;
|
||||||
|
if(hintVisible)
|
||||||
|
label.setAlpha(0f);
|
||||||
|
|
||||||
|
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onTextChanged(Editable text){
|
||||||
|
boolean newHintVisible=text.length()==0;
|
||||||
|
if(newHintVisible==hintVisible)
|
||||||
|
return;
|
||||||
|
if(currentAnim!=null)
|
||||||
|
currentAnim.cancel();
|
||||||
|
hintVisible=newHintVisible;
|
||||||
|
|
||||||
|
label.setAlpha(1);
|
||||||
|
float scale=edit.getLineHeight()/(float)label.getLineHeight();
|
||||||
|
float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
|
||||||
|
|
||||||
|
AnimatorSet anim=new AnimatorSet();
|
||||||
|
if(hintVisible){
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, 0),
|
||||||
|
ObjectAnimator.ofFloat(label, SCALE_X, scale),
|
||||||
|
ObjectAnimator.ofFloat(label, SCALE_Y, scale),
|
||||||
|
ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY)
|
||||||
|
);
|
||||||
|
edit.setHintTextColor(0);
|
||||||
|
}else{
|
||||||
|
label.setScaleX(scale);
|
||||||
|
label.setScaleY(scale);
|
||||||
|
label.setTranslationY(transY);
|
||||||
|
anim.playTogether(
|
||||||
|
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY),
|
||||||
|
ObjectAnimator.ofFloat(label, SCALE_X, 1f),
|
||||||
|
ObjectAnimator.ofFloat(label, SCALE_Y, 1f),
|
||||||
|
ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
anim.setDuration(150);
|
||||||
|
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||||
|
anim.start();
|
||||||
|
anim.addListener(new AnimatorListenerAdapter(){
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation){
|
||||||
|
currentAnim=null;
|
||||||
|
if(hintVisible){
|
||||||
|
label.setAlpha(0);
|
||||||
|
edit.setHintTextColor(label.getTextColors());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentAnim=anim;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.joinmastodon.android.ui.views;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
|
public class SplashLogoView extends ImageView{
|
||||||
|
private Bitmap shadow;
|
||||||
|
private Paint paint=new Paint();
|
||||||
|
|
||||||
|
public SplashLogoView(Context context){
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SplashLogoView(Context context, AttributeSet attrs){
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SplashLogoView(Context context, AttributeSet attrs, int defStyle){
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas){
|
||||||
|
if(shadow!=null){
|
||||||
|
paint.setColor(0xBF000000);
|
||||||
|
canvas.drawBitmap(shadow, 0, 0, paint);
|
||||||
|
}
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh){
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
if(w!=oldw || h!=oldh)
|
||||||
|
updateShadow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setImageDrawable(@Nullable Drawable drawable){
|
||||||
|
super.setImageDrawable(drawable);
|
||||||
|
updateShadow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateShadow(){
|
||||||
|
int w=getWidth();
|
||||||
|
int h=getHeight();
|
||||||
|
Drawable drawable=getDrawable();
|
||||||
|
if(w==0 || h==0 || drawable==null)
|
||||||
|
return;
|
||||||
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
|
Bitmap temp=Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8);
|
||||||
|
shadow=Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8);
|
||||||
|
Canvas c=new Canvas(temp);
|
||||||
|
c.translate(getWidth()/2f-drawable.getIntrinsicWidth()/2f, getHeight()/2f-drawable.getIntrinsicHeight()/2f);
|
||||||
|
drawable.draw(c);
|
||||||
|
c=new Canvas(shadow);
|
||||||
|
Paint paint=new Paint();
|
||||||
|
paint.setShadowLayer(V.dp(2), 0, 0, 0xff000000);
|
||||||
|
c.drawBitmap(temp, 0, 0, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/boost_selected" android:state_selected="true"/>
|
<item android:color="?android:colorAccent" android:state_selected="true"/>
|
||||||
<item android:color="?android:textColorSecondary" android:state_enabled="true"/>
|
<item android:color="?android:textColorSecondary" android:state_enabled="true"/>
|
||||||
<item android:color="?android:textColorSecondary" android:alpha="0.3"/>
|
<item android:color="?android:textColorSecondary" android:alpha="0.3"/>
|
||||||
</selector>
|
</selector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/gray_800" android:state_enabled="true"/>
|
<item android:color="?android:colorPrimary" android:state_enabled="true"/>
|
||||||
<item android:color="@color/gray_300"/>
|
<item android:color="?colorPollVoted"/>
|
||||||
</selector>
|
</selector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/gray_100" android:state_enabled="true"/>
|
<item android:color="?colorSecondary" android:state_enabled="true"/>
|
||||||
<item android:color="@color/gray_500"/>
|
<item android:color="?colorPollVoted"/>
|
||||||
</selector>
|
</selector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/gray_600" android:state_enabled="true"/>
|
<item android:color="?colorPollVoted" android:state_enabled="true"/>
|
||||||
<item android:color="@color/gray_300"/>
|
<item android:color="?colorSearchHint"/>
|
||||||
</selector>
|
</selector>
|
||||||
5
mastodon/src/main/res/color/button_text_m3_filled.xml
Normal file
5
mastodon/src/main/res/color/button_text_m3_filled.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3OnPrimary" android:state_enabled="true"/>
|
||||||
|
<item android:color="?colorM3OnSurface" android:alpha="0.38"/>
|
||||||
|
</selector>
|
||||||
5
mastodon/src/main/res/color/button_text_m3_text.xml
Normal file
5
mastodon/src/main/res/color/button_text_m3_text.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3Primary" android:state_enabled="true"/>
|
||||||
|
<item android:color="?colorM3OnSurface" android:alpha="0.38"/>
|
||||||
|
</selector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/gray_50" android:state_enabled="true"/>
|
<item android:color="@color/gray_50" android:state_enabled="true"/>
|
||||||
<item android:color="@color/gray_400"/>
|
<item android:color="?colorTabInactive"/>
|
||||||
</selector>
|
</selector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/gray_800" android:state_enabled="true"/>
|
<item android:color="@color/gray_800" android:state_enabled="true"/>
|
||||||
<item android:color="@color/gray_400"/>
|
<item android:color="?colorTabInactive"/>
|
||||||
</selector>
|
</selector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/gray_800" android:state_enabled="true"/>
|
<item android:color="?android:colorPrimary" android:state_enabled="true"/>
|
||||||
<item android:color="@color/gray_400"/>
|
<item android:color="?colorTabInactive"/>
|
||||||
</selector>
|
</selector>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="@color/gray_50"/>
|
<item android:color="?colorSecondary"/>
|
||||||
</selector>
|
</selector>
|
||||||
4
mastodon/src/main/res/color/m3_pressed_overlay.xml
Normal file
4
mastodon/src/main/res/color/m3_pressed_overlay.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3PressedOverlay" android:alpha="0.12"/>
|
||||||
|
</selector>
|
||||||
4
mastodon/src/main/res/color/m3_primary_overlay.xml
Normal file
4
mastodon/src/main/res/color/m3_primary_overlay.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3Primary" android:alpha="0.12"/>
|
||||||
|
</selector>
|
||||||
5
mastodon/src/main/res/color/m3_radiobutton_tint.xml
Normal file
5
mastodon/src/main/res/color/m3_radiobutton_tint.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3Primary" android:state_checked="true"/>
|
||||||
|
<item android:color="?colorM3OnSurfaceVariant"/>
|
||||||
|
</selector>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
19
mastodon/src/main/res/drawable/bg_button_m3_filled.xml
Normal file
19
mastodon/src/main/res/drawable/bg_button_m3_filled.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="true">
|
||||||
|
<ripple android:color="@color/m3_pressed_overlay">
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="?colorM3Primary"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="?colorM3DisabledBackground"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
9
mastodon/src/main/res/drawable/bg_button_m3_text.xml
Normal file
9
mastodon/src/main/res/drawable/bg_button_m3_text.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple android:color="@color/m3_primary_overlay" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#000"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user