Compare commits
172 Commits
v1.2.3+for
...
v1.2.3+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8115578e94 | ||
|
|
08dc122b6b | ||
|
|
e3199c009e | ||
|
|
2e09010151 | ||
|
|
3d79e87ec8 | ||
|
|
09ed3c647a | ||
|
|
172515ba0a | ||
|
|
a2f0fc8c87 | ||
|
|
888cee4556 | ||
|
|
c005e9bf18 | ||
|
|
64362968fc | ||
|
|
320027ca9b | ||
|
|
ad2678da7c | ||
|
|
c56c7448d0 | ||
|
|
731e67725c | ||
|
|
8178f81c85 | ||
|
|
6fbf00a132 | ||
|
|
968cde9e4c | ||
|
|
f2ab2acef7 | ||
|
|
81d1ecc5f8 | ||
|
|
513e6439ff | ||
|
|
1f0108b14e | ||
|
|
1433d0717e | ||
|
|
be91775f4b | ||
|
|
7b2f8d2be3 | ||
|
|
54c386ccec | ||
|
|
9d9e98959f | ||
|
|
a3cd7224bd | ||
|
|
6cf214f127 | ||
|
|
b6976fb519 | ||
|
|
871ada23ab | ||
|
|
040237de2b | ||
|
|
29b2a25840 | ||
|
|
bd2f05be67 | ||
|
|
ed075b276f | ||
|
|
dd25f3380a | ||
|
|
1a2d1efa29 | ||
|
|
f3e1fa4b2b | ||
|
|
fc307ff43f | ||
|
|
04304b3397 | ||
|
|
23f4b63195 | ||
|
|
edeae13dda | ||
|
|
44558534e9 | ||
|
|
2b760bb215 | ||
|
|
44154a987d | ||
|
|
7260db6668 | ||
|
|
1dadc51ddf | ||
|
|
fe85351869 | ||
|
|
74a83c6ac4 | ||
|
|
acb1369e88 | ||
|
|
8e0d74f9c2 | ||
|
|
df77ba61ad | ||
|
|
ed40f74d59 | ||
|
|
42e26bef68 | ||
|
|
af60adb55f | ||
|
|
b94741feae | ||
|
|
e43d6c35d8 | ||
|
|
4a6f9e80b1 | ||
|
|
ec02680507 | ||
|
|
5fc569a45a | ||
|
|
4bc9c5691d | ||
|
|
19b68855ac | ||
|
|
70fdfb612e | ||
|
|
0a32c217d8 | ||
|
|
5dfa9237ad | ||
|
|
573ff75498 | ||
|
|
87c37df370 | ||
|
|
7fb0944e66 | ||
|
|
35c8a3d121 | ||
|
|
9e58413d1a | ||
|
|
90e60aef84 | ||
|
|
8547ce05ed | ||
|
|
0825faee5c | ||
|
|
d43a697df7 | ||
|
|
3b742c4391 | ||
|
|
a43a396043 | ||
|
|
bcb4fac553 | ||
|
|
35bf858a83 | ||
|
|
870bfaf08c | ||
|
|
c4238fb19b | ||
|
|
ba7aeb358b | ||
|
|
6f3fd4d454 | ||
|
|
c890195567 | ||
|
|
b50a327b17 | ||
|
|
97547f334f | ||
|
|
1ab953d819 | ||
|
|
dbe7eb25ff | ||
|
|
45ecec09f5 | ||
|
|
9b4556d293 | ||
|
|
307d483a56 | ||
|
|
9612248695 | ||
|
|
1f63401e5b | ||
|
|
d35ec18a88 | ||
|
|
b93b1847c3 | ||
|
|
cd46ed565f | ||
|
|
4a0e4edef8 | ||
|
|
2ea7333daa | ||
|
|
fa7a66809d | ||
|
|
71884ab760 | ||
|
|
f31205c670 | ||
|
|
0091ae87ce | ||
|
|
ad13b1e927 | ||
|
|
a354ea80ab | ||
|
|
9f65b8112a | ||
|
|
6ac5d957fe | ||
|
|
4258c55b88 | ||
|
|
969f29e2e9 | ||
|
|
68921d0f0b | ||
|
|
c4ac4ee173 | ||
|
|
659b4e2fcd | ||
|
|
24e5bda8d3 | ||
|
|
02b1ad8d7a | ||
|
|
47eeb01b75 | ||
|
|
4288814138 | ||
|
|
ac4458e106 | ||
|
|
3d24b2de10 | ||
|
|
ed994b23e9 | ||
|
|
8c4678aba5 | ||
|
|
3d5fb2dfea | ||
|
|
ef6238b593 | ||
|
|
bc9bec3d66 | ||
|
|
d16e199dd1 | ||
|
|
a9c2df2e83 | ||
|
|
4673a4b9f7 | ||
|
|
d4a5286895 | ||
|
|
1b4579346b | ||
|
|
0665b8dd3b | ||
|
|
853124e2ce | ||
|
|
5dcd6e5a0d | ||
|
|
6f25c8be0f | ||
|
|
1db4b1319e | ||
|
|
76a97fcb47 | ||
|
|
4baaa39f35 | ||
|
|
52f025ae5a | ||
|
|
14b805e883 | ||
|
|
433a7b15fe | ||
|
|
6c8cbbc34a | ||
|
|
d4fbb298c1 | ||
|
|
2aeb5f03d6 | ||
|
|
6522403c37 | ||
|
|
f090ca7f75 | ||
|
|
2f02a238df | ||
|
|
0d5fa97800 | ||
|
|
b102deaee1 | ||
|
|
968b2ee460 | ||
|
|
890340de94 | ||
|
|
4ca1a7b29e | ||
|
|
5432f2590c | ||
|
|
60ccf5cf0a | ||
|
|
bc717f5b10 | ||
|
|
486eef21dd | ||
|
|
44a4d02815 | ||
|
|
336a8194bd | ||
|
|
7859f4cd05 | ||
|
|
37622ba9ce | ||
|
|
7a6af89375 | ||
|
|
056bfaacfe | ||
|
|
6684311ec5 | ||
|
|
11943571ad | ||
|
|
f696fcd412 | ||
|
|
2919e109ca | ||
|
|
995f478708 | ||
|
|
fb8764bcd7 | ||
|
|
d7f73e02c5 | ||
|
|
e897b3af57 | ||
|
|
e04fd8a004 | ||
|
|
ada70ae1b5 | ||
|
|
5fdec0900e | ||
|
|
fdbf331432 | ||
|
|
aed86ac6f0 | ||
|
|
3a13d4d6c0 | ||
|
|
f5336564d0 |
52
README.md
52
README.md
@@ -10,44 +10,47 @@
|
|||||||
|
|
||||||
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||||
|
|
||||||
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly won’t ever be implemented, such as the federated timeline, unlisted posting and an image description viewer.
|
> A fork of the [Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app, focusing on [Glitch](https://glitch-soc.github.io/docs) compatibility, a pretty UI and adding new features that I feel make using the Fediverse a more pleasent experience.
|
||||||
|
|
||||||
|
|
||||||
## Key features
|
## Key features
|
||||||
|
|
||||||
### **Unlisted posting**
|
### **Unlisted posting**
|
||||||
|
|
||||||
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).**
|
<details>
|
||||||
|
<p><summary>Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Community”, “Federated” and “Posts”).</summary></p>
|
||||||
|
|
||||||
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in people’s Home timelines, but only if they follow you or someone they follow reblogged/replied to your post.
|
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in people’s Home timelines, but only if they follow you or someone they follow reblogged/replied to your post.
|
||||||
|
|
||||||
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
|
The Mastodon documentation has some more information about [Unlisted posting](https://docs.joinmastodon.org/user/posting/#unlisted) and [Public timelines](https://docs.joinmastodon.org/user/network/#timelines).
|
||||||
|
</details>
|
||||||
|
|
||||||
### **Federated timeline**
|
### **Federated timeline**
|
||||||
|
|
||||||
**This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.**
|
<details>
|
||||||
|
<p><summary>This allows you to chronologically see all Public posts from people on all other Fediverse neighborhoods your home instance is connected to.</summary></p>
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
That’s one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
|
That’s one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### **Customizable timelines**
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<p><summary>You can customize Megalodon’s home tab and not only add local and federated timelines, but also pin lists and hashtags.</summary></p>
|
||||||
|
|
||||||
|
Even better: You can rename every timeline however you please and pick a distinct icon for each timeline. This way, you can pin the hashtag “#Caturday”, rename your timeline to “CUTENESS OVERLOAD” and set <img src="img/ic_fluent_animal_cat_24_regular.svg" alt="Cat icon from Microsoft Fluent UI icons"> as its icon. :3 You can find the timelines editor by opening your home tab, tapping the `⋮` button in the top right and going to “Edit timelines”.
|
||||||
|
</details>
|
||||||
|
|
||||||
### **Draft and schedule posts**
|
### **Draft and schedule posts**
|
||||||
|
|
||||||
**Allows for preparing a post and scheduling it to send it automatically at a specific time.**
|
<details>
|
||||||
|
<p><summary>
|
||||||
|
Allows to prepare a post and schedule it to send it automatically at a specific time.</summary></p>
|
||||||
|
|
||||||
You can create drafts, edit them, send them manually later or set a scheduled date. Drafts are technically saved as scheduled posts, so you can view and edit them from other apps that support scheduled posts. Scheduled posts are handled by your home instance, so they'll work even if you uninstall Megalodon.
|
You can create drafts, edit them, send them manually later or set a scheduled date. Drafts are technically saved as scheduled posts, so you can view and edit them from other apps that support scheduled posts. Scheduled posts are handled by your home instance, so they'll work even if you uninstall Megalodon.
|
||||||
|
</details>
|
||||||
### **Image description viewer**
|
|
||||||
|
|
||||||
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
|
|
||||||
|
|
||||||
This is important to **ensure the content you’re sharing is as accessible as possible** to people who can’t see the images and rely on software to read back the provided content descriptions. Thankfully, it’s quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
|
|
||||||
|
|
||||||
### **Pinning posts**
|
|
||||||
|
|
||||||
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in people’s profiles shows all the posts they pinned.**
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -88,7 +91,7 @@ Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastod
|
|||||||
|
|
||||||
## Release variants
|
## Release variants
|
||||||
|
|
||||||
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. When downloading a pre-release, expect to see unfinished features and bugs. If you don’t want that, just download the [latest full release](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk).
|
||||||
|
|
||||||
**`megalodon.apk`**
|
**`megalodon.apk`**
|
||||||
|
|
||||||
@@ -108,11 +111,11 @@ Variant without the integrated updater. This is the variant to be published to F
|
|||||||
|
|
||||||
### Translation
|
### 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
|
The translation for the base of the app is sourced from the upstream **Mastodon for Android** project, which you can contribute to on its Crowdin project: [https://crowdin.com/project/mastodon-for-android](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/
|
There's also a bunch of custom strings exclusive to this project that need to be translated. You can help translate **Megalodon** on Weblate: [https://translate.codeberg.org/projects/megalodon](https://translate.codeberg.org/projects/megalodon)
|
||||||
|
|
||||||
[](https://translate.codeberg.org/engage/megalodon/)
|
[](https://translate.codeberg.org/engage/megalodon)
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -154,6 +157,10 @@ There's also a handful of custom strings exclusive to this projects that would n
|
|||||||
* [Soft-blocking (by blocking and immediately unblocking)](https://github.com/sk22/megalodon/commit/e75d350b7a2709259e9fc5138e0e1f361bdb0972)
|
* [Soft-blocking (by blocking and immediately unblocking)](https://github.com/sk22/megalodon/commit/e75d350b7a2709259e9fc5138e0e1f361bdb0972)
|
||||||
* [Pinnable custom timelines](https://github.com/sk22/megalodon/pull/338/commits)
|
* [Pinnable custom timelines](https://github.com/sk22/megalodon/pull/338/commits)
|
||||||
* Support for local-only posts
|
* Support for local-only posts
|
||||||
|
* Support for copying the URL to posts/accounts/… in Pixel launcher’s Recent apps view
|
||||||
|
* Compatibility for Akkoma Bubble timeline
|
||||||
|
* Listings of followers/following/favorites/boosts can be loaded from the origin instance (there’s an option to disable this in in the settings)
|
||||||
|
* Allow opening posts/accounts in-app by sharing a URL/handle to Megalodon (Originally implemented in [Moshidon](https://github.com/LucasGGamerM/moshidon), [PR](https://github.com/sk22/megalodon/pull/531))
|
||||||
|
|
||||||
|
|
||||||
### Behavior
|
### Behavior
|
||||||
@@ -179,6 +186,8 @@ There's also a handful of custom strings exclusive to this projects that would n
|
|||||||
* Improved filtering using Mastodon 4.0 API: [#202](https://github.com/sk22/megalodon/pull/202), [#212](https://github.com/sk22/megalodon/pull/212), [#255](https://github.com/sk22/megalodon/pull/255) by [@thiagojedi](https://github.com/thiagojedi)
|
* Improved filtering using Mastodon 4.0 API: [#202](https://github.com/sk22/megalodon/pull/202), [#212](https://github.com/sk22/megalodon/pull/212), [#255](https://github.com/sk22/megalodon/pull/255) by [@thiagojedi](https://github.com/thiagojedi)
|
||||||
* [Support admin notifications](https://github.com/sk22/megalodon/commit/c12a6eaee6b609bc53eb0a45d9199f37d5241801) and [notifications for edited reblogged posts](https://github.com/sk22/megalodon/commit/900e8fb2e9353002c16d15e06b78d2731e121601)
|
* [Support admin notifications](https://github.com/sk22/megalodon/commit/c12a6eaee6b609bc53eb0a45d9199f37d5241801) and [notifications for edited reblogged posts](https://github.com/sk22/megalodon/commit/900e8fb2e9353002c16d15e06b78d2731e121601)
|
||||||
* [Android file opener added back in addition to image picker](https://github.com/sk22/megalodon/commit/3a6ace53d5ab01e28077c9c930cb6ed487b78031)
|
* [Android file opener added back in addition to image picker](https://github.com/sk22/megalodon/commit/3a6ace53d5ab01e28077c9c930cb6ed487b78031)
|
||||||
|
* [Replies are inserted below the replied-to post in thread view](https://github.com/sk22/megalodon/commit/87c37df370ec24aeea0d2dbaeb29468aa4fb5808)
|
||||||
|
* Option to auto-reveal equal content warnings in threads
|
||||||
|
|
||||||
|
|
||||||
### Visual
|
### Visual
|
||||||
@@ -196,6 +205,7 @@ There's also a handful of custom strings exclusive to this projects that would n
|
|||||||
* Scale text according to system settings
|
* Scale text according to system settings
|
||||||
* Header in timeline for followed hashtags
|
* Header in timeline for followed hashtags
|
||||||
* [Indicator for missing alt texts](https://github.com/sk22/megalodon/commit/c0c276f03e793b78c478c17dfdef24a66ef7cedb)
|
* [Indicator for missing alt texts](https://github.com/sk22/megalodon/commit/c0c276f03e793b78c478c17dfdef24a66ef7cedb)
|
||||||
|
* Visually grouped (by removing divider lines and reducing padding) threaded replies in thread view
|
||||||
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
@@ -206,6 +216,8 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
|
|||||||
./gradlew assembleRelease
|
./gradlew assembleRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that Megalodon might be depending on an in-development version of [AppKit](https://github.com/grishka/appkit) – a library by Mastodon for Android’s developer. In case the used AppKit version isn’t published to Maven Central yet, you might have to clone, build and publish it to your local Maven repository. For more information, see [this GitHub issue](https://github.com/mastodon/mastodon-android/issues/375#issuecomment-1507678585).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is released under the [GPL-3 License](./LICENSE).
|
This project is released under the [GPL-3 License](./LICENSE).
|
||||||
|
|||||||
9
img/ic_fluent_animal_cat_24_regular.svg
Normal file
9
img/ic_fluent_animal_cat_24_regular.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
path { fill: black; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: white; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="M15.4925 3.50673C14.652 3.58251 13.9933 4.28929 13.9933 5.15V10C13.9933 10.4142 13.6577 10.75 13.2437 10.75C11.8002 10.75 10.7863 11.3378 10.0365 12.238C9.26389 13.1656 8.7607 14.444 8.44554 15.7954C8.13254 17.1376 8.01871 18.4912 7.98453 19.5172C7.97182 19.8987 7.9702 20.2324 7.97313 20.5H14.9928V19.75C14.9928 18.5074 13.986 17.5 12.744 17.5H11.4947C11.0807 17.5 10.7451 17.1642 10.7451 16.75C10.7451 16.3358 11.0807 16 11.4947 16H12.744C14.8139 16 16.4919 17.6789 16.4919 19.75V20.5H17.2415C17.6555 20.5 17.9911 20.1642 17.9911 19.75V9.75C17.9911 9.33579 18.3267 9 18.7407 9H19.2472C20.2264 9 20.8249 7.92404 20.309 7.09132L19.6893 6.09132C19.4615 5.72367 19.0599 5.5 18.6275 5.5H16.2421C15.8281 5.5 15.4925 5.16421 15.4925 4.75V3.50673ZM6.47388 20.5C6.47098 20.2156 6.47293 19.8655 6.4862 19.4672C6.52229 18.3838 6.64271 16.9249 6.98559 15.4546C7.32631 13.9935 7.90065 12.4594 8.88484 11.2777C9.75681 10.2307 10.9399 9.47669 12.4942 9.29318V5.15C12.4942 3.4103 13.9037 2 15.6424 2C16.3876 2 16.9916 2.60442 16.9916 3.35V4H18.6275C19.5787 4 20.4622 4.49207 20.9634 5.30092L21.5831 6.30092C22.6749 8.06291 21.4985 10.32 19.4903 10.4898V19.75C19.4903 20.9926 18.4835 22 17.2415 22H7.24708L7.24537 22H5.79625C3.69964 22 2 20.2994 2 18.2016C2 17.2395 2.36489 16.3133 3.02098 15.6099L4.15612 14.393C4.92005 13.5741 5.17521 12.4027 4.82117 11.3399C4.67114 10.8896 4.41837 10.4804 4.08288 10.1447L2.96914 9.03042C2.67641 8.73753 2.67639 8.26266 2.96912 7.96976C3.26184 7.67686 3.73645 7.67685 4.02919 7.96974L5.14293 9.08405C5.643 9.58438 6.01977 10.1943 6.2434 10.8656C6.77114 12.4497 6.3908 14.1958 5.25209 15.4165L4.11695 16.6334C3.71996 17.059 3.49916 17.6195 3.49916 18.2016C3.49916 19.471 4.52761 20.5 5.79625 20.5H6.47388Z" fill="#212121"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId "org.joinmastodon.android.sk"
|
applicationId "org.joinmastodon.android.sk"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 87
|
versionCode 94
|
||||||
versionName "1.2.3+fork.87"
|
versionName "1.2.3+fork.94"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ dependencies {
|
|||||||
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
|
||||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||||
implementation 'me.grishka.appkit:appkit:1.2.7'
|
implementation 'me.grishka.appkit:appkit:1.2.8'
|
||||||
implementation 'com.google.code.gson:gson:2.9.0'
|
implementation 'com.google.code.gson:gson:2.9.0'
|
||||||
implementation 'org.jsoup:jsoup:1.14.3'
|
implementation 'org.jsoup:jsoup:1.14.3'
|
||||||
implementation 'com.squareup:otto:1.3.8'
|
implementation 'com.squareup:otto:1.3.8'
|
||||||
|
|||||||
4
mastodon/proguard-rules.pro
vendored
4
mastodon/proguard-rules.pro
vendored
@@ -46,3 +46,7 @@
|
|||||||
-keep interface org.parceler.Parcel
|
-keep interface org.parceler.Parcel
|
||||||
-keep @org.parceler.Parcel class * { *; }
|
-keep @org.parceler.Parcel class * { *; }
|
||||||
-keep class **$$Parcelable { *; }
|
-keep class **$$Parcelable { *; }
|
||||||
|
|
||||||
|
-keep class com.google.gson.reflect.TypeToken
|
||||||
|
-keep class * extends com.google.gson.reflect.TypeToken
|
||||||
|
-keep public class * implements java.lang.reflect.Type
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ package org.joinmastodon.android.fragments;
|
|||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
import android.util.Pair;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.StatusContext;
|
import org.joinmastodon.android.model.StatusContext;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class ThreadFragmentTest {
|
public class ThreadFragmentTest {
|
||||||
|
|
||||||
@@ -20,10 +20,7 @@ public class ThreadFragmentTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
|
private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) {
|
||||||
ThreadFragment.NeighborAncestryInfo info = new ThreadFragment.NeighborAncestryInfo(s);
|
return new ThreadFragment.NeighborAncestryInfo(s, d, a);
|
||||||
info.descendantNeighbor = d;
|
|
||||||
info.ancestoringNeighbor = a;
|
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -55,6 +52,33 @@ public class ThreadFragmentTest {
|
|||||||
), neighbors);
|
), neighbors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeApplyMainStatus() {
|
||||||
|
ThreadFragment fragment = new ThreadFragment();
|
||||||
|
fragment.contextInitiallyRendered = true;
|
||||||
|
fragment.mainStatus = Status.ofFake("123456", "original text", Instant.EPOCH);
|
||||||
|
|
||||||
|
Status update1 = Status.ofFake("123456", "updated text", Instant.EPOCH);
|
||||||
|
update1.editedAt = Instant.ofEpochSecond(1);
|
||||||
|
fragment.updatedStatus = update1;
|
||||||
|
StatusUpdatedEvent event1 = (StatusUpdatedEvent) fragment.maybeApplyMainStatus();
|
||||||
|
assertEquals("fired update event", update1, event1.status);
|
||||||
|
assertEquals("updated main status", update1, fragment.mainStatus);
|
||||||
|
|
||||||
|
Status update2 = Status.ofFake("123456", "updated text", Instant.EPOCH);
|
||||||
|
update2.favouritesCount = 123;
|
||||||
|
fragment.updatedStatus = update2;
|
||||||
|
StatusCountersUpdatedEvent event2 = (StatusCountersUpdatedEvent) fragment.maybeApplyMainStatus();
|
||||||
|
assertEquals("only fired counter update event", update2.id, event2.id);
|
||||||
|
assertEquals("updated counter is correct", 123, event2.favorites);
|
||||||
|
assertEquals("updated main status", update2, fragment.mainStatus);
|
||||||
|
|
||||||
|
Status update3 = Status.ofFake("123456", "whatever", Instant.EPOCH);
|
||||||
|
fragment.contextInitiallyRendered = false;
|
||||||
|
fragment.updatedStatus = update3;
|
||||||
|
assertNull("no update when context hasn't been rendered", fragment.maybeApplyMainStatus());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void sortStatusContext() {
|
public void sortStatusContext() {
|
||||||
StatusContext context = new StatusContext();
|
StatusContext context = new StatusContext();
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package org.joinmastodon.android.ui.utils;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Instance;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class UiUtilsTest {
|
||||||
|
@BeforeClass
|
||||||
|
public static void createDummySession() {
|
||||||
|
Instance dummyInstance = new Instance();
|
||||||
|
dummyInstance.uri = "test.tld";
|
||||||
|
Account dummyAccount = new Account();
|
||||||
|
dummyAccount.id = "123456";
|
||||||
|
AccountSessionManager.getInstance().addAccount(dummyInstance, null, dummyAccount, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void cleanUp() {
|
||||||
|
AccountSessionManager.getInstance().removeAccount("test.tld_123456");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseFediverseHandle() {
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||||
|
UiUtils.parseFediverseHandle("megalodon@floss.social")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||||
|
UiUtils.parseFediverseHandle("@megalodon@floss.social")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(Pair.create("megalodon", Optional.empty())),
|
||||||
|
UiUtils.parseFediverseHandle("@megalodon")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(Pair.create("megalodon", Optional.of("floss.social"))),
|
||||||
|
UiUtils.parseFediverseHandle("mailto:megalodon@floss.social")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.empty(),
|
||||||
|
UiUtils.parseFediverseHandle("megalodon")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.empty(),
|
||||||
|
UiUtils.parseFediverseHandle("this is not a fedi handle")
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Optional.empty(),
|
||||||
|
UiUtils.parseFediverseHandle("not@a-domain")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acctMatches() {
|
||||||
|
assertTrue("local account, domain not specified", UiUtils.acctMatches(
|
||||||
|
"test.tld_123456",
|
||||||
|
"someone",
|
||||||
|
"someone",
|
||||||
|
null
|
||||||
|
));
|
||||||
|
|
||||||
|
assertTrue("domain not specified", UiUtils.acctMatches(
|
||||||
|
"test.tld_123456",
|
||||||
|
"someone@somewhere.social",
|
||||||
|
"someone",
|
||||||
|
null
|
||||||
|
));
|
||||||
|
|
||||||
|
assertTrue("local account, domain specified, different casing", UiUtils.acctMatches(
|
||||||
|
"test.tld_123456",
|
||||||
|
"SomeOne",
|
||||||
|
"someone",
|
||||||
|
"Test.TLD"
|
||||||
|
));
|
||||||
|
|
||||||
|
assertFalse("username doesn't match", UiUtils.acctMatches(
|
||||||
|
"test.tld_123456",
|
||||||
|
"someone-else@somewhere.social",
|
||||||
|
"someone",
|
||||||
|
"somewhere.social"
|
||||||
|
));
|
||||||
|
|
||||||
|
assertFalse("domain doesn't match", UiUtils.acctMatches(
|
||||||
|
"test.tld_123456",
|
||||||
|
"someone@somewhere.social",
|
||||||
|
"someone",
|
||||||
|
"somewhere.else"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Pair;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
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;
|
||||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||||
@@ -19,6 +21,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
@@ -31,19 +34,23 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||||||
if(savedInstanceState==null){
|
if(savedInstanceState==null){
|
||||||
|
|
||||||
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
||||||
boolean isMastodonURL = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
|
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
|
||||||
|
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).orElse(false);
|
||||||
|
boolean isOpenable = isFediUrl || fediHandle.isPresent();
|
||||||
|
|
||||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||||
if(sessions.isEmpty()){
|
if (sessions.isEmpty()){
|
||||||
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
finish();
|
||||||
}else if(sessions.size()==1 && !isMastodonURL){
|
} else if (isOpenable || sessions.size() > 1) {
|
||||||
openComposeFragment(sessions.get(0).getID());
|
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
|
||||||
}else{
|
sheet.setOnClick((accountId, open) -> {
|
||||||
new AccountSwitcherSheet(this, null, true, isMastodonURL, (accountId, open) -> {
|
|
||||||
if (open && text.isPresent()) {
|
if (open && text.isPresent()) {
|
||||||
UiUtils.lookupURL(this, accountId, text.get(), false, (clazz, args) -> {
|
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
|
||||||
if (clazz == null) {
|
if (clazz == null) {
|
||||||
|
Toast.makeText(this, R.string.sk_open_in_app_failed, Toast.LENGTH_SHORT).show();
|
||||||
|
// TODO: do something about the window getting leaked
|
||||||
|
sheet.dismiss();
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,11 +59,25 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||||||
intent.putExtras(args);
|
intent.putExtras(args);
|
||||||
finish();
|
finish();
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
});
|
};
|
||||||
|
|
||||||
|
fediHandle
|
||||||
|
.<MastodonAPIRequest<?>>map(handle ->
|
||||||
|
UiUtils.lookupAccountHandle(this, accountId, handle, callback))
|
||||||
|
.or(() ->
|
||||||
|
UiUtils.lookupURL(this, accountId, text.get(), callback))
|
||||||
|
.ifPresent(req ->
|
||||||
|
req.wrapProgress(this, R.string.loading, true, d -> {
|
||||||
|
UiUtils.transformDialogForLookup(this, accountId, isFediUrl ? text.get() : null, d);
|
||||||
|
d.setOnDismissListener((x) -> finish());
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
openComposeFragment(accountId);
|
openComposeFragment(accountId);
|
||||||
}
|
}
|
||||||
}).show();
|
});
|
||||||
|
sheet.show();
|
||||||
|
} else if (sessions.size() == 1) {
|
||||||
|
openComposeFragment(sessions.get(0).getID());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean showAltIndicator;
|
public static boolean showAltIndicator;
|
||||||
public static boolean showNoAltIndicator;
|
public static boolean showNoAltIndicator;
|
||||||
public static boolean enablePreReleases;
|
public static boolean enablePreReleases;
|
||||||
public static boolean prefixRepliesWithRe;
|
public static PrefixRepliesMode prefixReplies;
|
||||||
public static boolean bottomEncoding;
|
public static boolean bottomEncoding;
|
||||||
public static boolean collapseLongPosts;
|
public static boolean collapseLongPosts;
|
||||||
public static boolean spectatorMode;
|
public static boolean spectatorMode;
|
||||||
@@ -48,13 +48,13 @@ public class GlobalUserPreferences{
|
|||||||
public static boolean replyLineAboveHeader;
|
public static boolean replyLineAboveHeader;
|
||||||
public static boolean compactReblogReplyLine;
|
public static boolean compactReblogReplyLine;
|
||||||
public static boolean confirmBeforeReblog;
|
public static boolean confirmBeforeReblog;
|
||||||
|
public static boolean allowRemoteLoading;
|
||||||
|
public static boolean forwardReportDefault;
|
||||||
|
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||||
public static String publishButtonText;
|
public static String publishButtonText;
|
||||||
public static ThemePreference theme;
|
public static ThemePreference theme;
|
||||||
public static ColorPreference color;
|
public static ColorPreference color;
|
||||||
|
|
||||||
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
|
|
||||||
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
|
|
||||||
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
|
|
||||||
public static Map<String, List<String>> recentLanguages;
|
public static Map<String, List<String>> recentLanguages;
|
||||||
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
||||||
public static Set<String> accountsWithLocalOnlySupport;
|
public static Set<String> accountsWithLocalOnlySupport;
|
||||||
@@ -62,6 +62,10 @@ public class GlobalUserPreferences{
|
|||||||
public static Set<String> accountsWithContentTypesEnabled;
|
public static Set<String> accountsWithContentTypesEnabled;
|
||||||
public static Map<String, ContentType> accountsDefaultContentTypes;
|
public static Map<String, ContentType> accountsDefaultContentTypes;
|
||||||
|
|
||||||
|
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
|
||||||
|
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
|
||||||
|
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pleroma
|
* Pleroma
|
||||||
*/
|
*/
|
||||||
@@ -110,7 +114,7 @@ public class GlobalUserPreferences{
|
|||||||
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
|
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
|
||||||
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
|
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
|
||||||
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
|
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
|
||||||
prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false);
|
prefixReplies=PrefixRepliesMode.valueOf(prefs.getString("prefixReplies", PrefixRepliesMode.NEVER.name()));
|
||||||
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
||||||
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
||||||
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
||||||
@@ -127,6 +131,18 @@ public class GlobalUserPreferences{
|
|||||||
replyVisibility=prefs.getString("replyVisibility", null);
|
replyVisibility=prefs.getString("replyVisibility", null);
|
||||||
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
|
||||||
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
|
||||||
|
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||||
|
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||||
|
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
||||||
|
|
||||||
|
if (prefs.contains("prefixRepliesWithRe")) {
|
||||||
|
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
|
||||||
|
? PrefixRepliesMode.TO_OTHERS : PrefixRepliesMode.NEVER;
|
||||||
|
prefs.edit()
|
||||||
|
.putString("prefixReplies", prefixReplies.name())
|
||||||
|
.remove("prefixRepliesWithRe")
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
||||||
@@ -158,7 +174,7 @@ public class GlobalUserPreferences{
|
|||||||
.putBoolean("showAltIndicator", showAltIndicator)
|
.putBoolean("showAltIndicator", showAltIndicator)
|
||||||
.putBoolean("showNoAltIndicator", showNoAltIndicator)
|
.putBoolean("showNoAltIndicator", showNoAltIndicator)
|
||||||
.putBoolean("enablePreReleases", enablePreReleases)
|
.putBoolean("enablePreReleases", enablePreReleases)
|
||||||
.putBoolean("prefixRepliesWithRe", prefixRepliesWithRe)
|
.putString("prefixReplies", prefixReplies.name())
|
||||||
.putBoolean("collapseLongPosts", collapseLongPosts)
|
.putBoolean("collapseLongPosts", collapseLongPosts)
|
||||||
.putBoolean("spectatorMode", spectatorMode)
|
.putBoolean("spectatorMode", spectatorMode)
|
||||||
.putBoolean("autoHideFab", autoHideFab)
|
.putBoolean("autoHideFab", autoHideFab)
|
||||||
@@ -176,6 +192,9 @@ public class GlobalUserPreferences{
|
|||||||
.putString("replyVisibility", replyVisibility)
|
.putString("replyVisibility", replyVisibility)
|
||||||
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
|
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
|
||||||
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
|
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
|
||||||
|
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||||
|
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||||
|
.putBoolean("forwardReportDefault", forwardReportDefault)
|
||||||
.apply();
|
.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,4 +214,16 @@ public class GlobalUserPreferences{
|
|||||||
LIGHT,
|
LIGHT,
|
||||||
DARK
|
DARK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum AutoRevealMode {
|
||||||
|
NEVER,
|
||||||
|
THREADS,
|
||||||
|
DISCUSSIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PrefixRepliesMode {
|
||||||
|
NEVER,
|
||||||
|
ALWAYS,
|
||||||
|
TO_OTHERS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,30 +117,18 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showFragmentForNotification(Notification notification, String accountID){
|
private void showFragmentForNotification(Notification notification, String accountID){
|
||||||
Fragment fragment;
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
args.putBoolean("_can_go_back", true);
|
|
||||||
try{
|
try{
|
||||||
notification.postprocess();
|
notification.postprocess();
|
||||||
}catch(ObjectValidationException x){
|
}catch(ObjectValidationException x){
|
||||||
Log.w("MainActivity", x);
|
Log.w("MainActivity", x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(notification.status!=null){
|
UiUtils.showFragmentForNotification(this, notification, accountID, null);
|
||||||
fragment=new ThreadFragment();
|
|
||||||
args.putParcelable("status", Parcels.wrap(notification.status));
|
|
||||||
}else{
|
|
||||||
fragment=new ProfileFragment();
|
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(notification.account));
|
|
||||||
}
|
|
||||||
fragment.setArguments(args);
|
|
||||||
showFragment(fragment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFragmentForExternalShare(Bundle args) {
|
private void showFragmentForExternalShare(Bundle args) {
|
||||||
String clazz = args.getString("fromExternalShare");
|
String className = args.getString("fromExternalShare");
|
||||||
Fragment fragment = switch (clazz) {
|
Fragment fragment = switch (className) {
|
||||||
case "ThreadFragment" -> new ThreadFragment();
|
case "ThreadFragment" -> new ThreadFragment();
|
||||||
case "ProfileFragment" -> new ProfileFragment();
|
case "ProfileFragment" -> new ProfileFragment();
|
||||||
default -> null;
|
default -> null;
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Token token){
|
public void onSuccess(Token token){
|
||||||
new GetOwnAccount()
|
new GetOwnAccount()
|
||||||
|
// in case the instance (looking at pixelfed) wants to redirect to a
|
||||||
|
// website, we need to pass a context so we can launch a browser
|
||||||
|
.setContext(OAuthActivity.this)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Account account){
|
public void onSuccess(Account account){
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.joinmastodon.android;
|
package org.joinmastodon.android;
|
||||||
|
|
||||||
|
import static org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode.*;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationChannelGroup;
|
import android.app.NotificationChannelGroup;
|
||||||
@@ -295,7 +297,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||||||
req.language = preferences.postingDefaultLanguage;
|
req.language = preferences.postingDefaultLanguage;
|
||||||
req.visibility = preferences.postingDefaultVisibility;
|
req.visibility = preferences.postingDefaultVisibility;
|
||||||
req.inReplyToId = notification.status.id;
|
req.inReplyToId = notification.status.id;
|
||||||
if(!notification.status.spoilerText.isEmpty() && GlobalUserPreferences.prefixRepliesWithRe && !notification.status.spoilerText.startsWith("re: ")){
|
|
||||||
|
if (!notification.status.spoilerText.isEmpty() &&
|
||||||
|
(GlobalUserPreferences.prefixReplies == ALWAYS
|
||||||
|
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(notification.status.account.id)))
|
||||||
|
&& !notification.status.spoilerText.startsWith("re: ")) {
|
||||||
req.spoilerText = "re: " + notification.status.spoilerText;
|
req.spoilerText = "re: " + notification.status.spoilerText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import me.grishka.appkit.utils.WorkerThread;
|
|||||||
|
|
||||||
public class CacheController{
|
public class CacheController{
|
||||||
private static final String TAG="CacheController";
|
private static final String TAG="CacheController";
|
||||||
private static final int DB_VERSION=3;
|
private static final int DB_VERSION=4;
|
||||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ public class CacheController{
|
|||||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
SQLiteDatabase db=getOrOpenDatabase();
|
||||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||||
if(cursor.getCount()==count){
|
if(cursor.getCount()==count){
|
||||||
ArrayList<Status> result=new ArrayList<>();
|
ArrayList<Status> result=new ArrayList<>();
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
@@ -112,7 +112,7 @@ public class CacheController{
|
|||||||
runOnDbThread((db)->{
|
runOnDbThread((db)->{
|
||||||
if(clear)
|
if(clear)
|
||||||
db.delete("home_timeline", null, null);
|
db.delete("home_timeline", null, null);
|
||||||
ContentValues values=new ContentValues(3);
|
ContentValues values=new ContentValues(4);
|
||||||
for(Status s:posts){
|
for(Status s:posts){
|
||||||
values.put("id", s.id);
|
values.put("id", s.id);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||||
@@ -120,6 +120,7 @@ public class CacheController{
|
|||||||
if(s.hasGapAfter)
|
if(s.hasGapAfter)
|
||||||
flags|=POST_FLAG_GAP_AFTER;
|
flags|=POST_FLAG_GAP_AFTER;
|
||||||
values.put("flags", flags);
|
values.put("flags", flags);
|
||||||
|
values.put("time", s.createdAt.getEpochSecond());
|
||||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -134,7 +135,7 @@ public class CacheController{
|
|||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
SQLiteDatabase db=getOrOpenDatabase();
|
||||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||||
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||||
if(cursor.getCount()==count){
|
if(cursor.getCount()==count){
|
||||||
ArrayList<Notification> result=new ArrayList<>();
|
ArrayList<Notification> result=new ArrayList<>();
|
||||||
cursor.moveToFirst();
|
cursor.moveToFirst();
|
||||||
@@ -192,7 +193,7 @@ public class CacheController{
|
|||||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||||
if(clear)
|
if(clear)
|
||||||
db.delete(table, null, null);
|
db.delete(table, null, null);
|
||||||
ContentValues values=new ContentValues(3);
|
ContentValues values=new ContentValues(4);
|
||||||
for(Notification n:notifications){
|
for(Notification n:notifications){
|
||||||
if(n.type==null){
|
if(n.type==null){
|
||||||
continue;
|
continue;
|
||||||
@@ -200,6 +201,7 @@ public class CacheController{
|
|||||||
values.put("id", n.id);
|
values.put("id", n.id);
|
||||||
values.put("json", MastodonAPIController.gson.toJson(n));
|
values.put("json", MastodonAPIController.gson.toJson(n));
|
||||||
values.put("type", n.type.ordinal());
|
values.put("type", n.type.ordinal());
|
||||||
|
values.put("time", n.createdAt.getEpochSecond());
|
||||||
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -296,21 +298,24 @@ public class CacheController{
|
|||||||
CREATE TABLE `home_timeline` (
|
CREATE TABLE `home_timeline` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
db.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE `notifications_all` (
|
CREATE TABLE `notifications_all` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`type` INTEGER NOT NULL
|
`type` INTEGER NOT NULL,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
db.execSQL("""
|
db.execSQL("""
|
||||||
CREATE TABLE `notifications_mentions` (
|
CREATE TABLE `notifications_mentions` (
|
||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`type` INTEGER NOT NULL
|
`type` INTEGER NOT NULL,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
createRecentSearchesTable(db);
|
createRecentSearchesTable(db);
|
||||||
createPostsNotificationsTable(db);
|
createPostsNotificationsTable(db);
|
||||||
@@ -318,12 +323,16 @@ public class CacheController{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
|
||||||
if(oldVersion==1){
|
if(oldVersion<2){
|
||||||
createRecentSearchesTable(db);
|
createRecentSearchesTable(db);
|
||||||
}
|
}
|
||||||
if(oldVersion==2){
|
if(oldVersion<3){
|
||||||
|
// MEGALODON-SPECIFIC
|
||||||
createPostsNotificationsTable(db);
|
createPostsNotificationsTable(db);
|
||||||
}
|
}
|
||||||
|
if(oldVersion<4){
|
||||||
|
addTimeColumns(db);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||||
@@ -341,9 +350,21 @@ public class CacheController{
|
|||||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||||
`json` TEXT NOT NULL,
|
`json` TEXT NOT NULL,
|
||||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||||
`type` INTEGER NOT NULL
|
`type` INTEGER NOT NULL,
|
||||||
|
`time` INTEGER NOT NULL
|
||||||
)""");
|
)""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addTimeColumns(SQLiteDatabase db){
|
||||||
|
db.execSQL("DELETE FROM `home_timeline`");
|
||||||
|
db.execSQL("DELETE FROM `notifications_all`");
|
||||||
|
db.execSQL("DELETE FROM `notifications_mentions`");
|
||||||
|
db.execSQL("DELETE FROM `notifications_posts`");
|
||||||
|
db.execSQL("ALTER TABLE `home_timeline` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
db.execSQL("ALTER TABLE `notifications_all` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
db.execSQL("ALTER TABLE `notifications_posts` ADD `time` INTEGER NOT NULL DEFAULT 0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
|
|||||||
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
|
||||||
import org.joinmastodon.android.api.session.AccountSession;
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -161,6 +162,11 @@ public class MastodonAPIController{
|
|||||||
respObj=gson.fromJson(reader, req.respClass);
|
respObj=gson.fromJson(reader, req.respClass);
|
||||||
}
|
}
|
||||||
}catch(JsonIOException|JsonSyntaxException x){
|
}catch(JsonIOException|JsonSyntaxException x){
|
||||||
|
if (req.context != null && response.body().contentType().subtype().equals("html")) {
|
||||||
|
UiUtils.launchWebBrowser(req.context, response.request().url().toString());
|
||||||
|
req.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(BuildConfig.DEBUG)
|
if(BuildConfig.DEBUG)
|
||||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.api;
|
|||||||
|
|
||||||
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.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
@@ -20,9 +21,11 @@ import java.util.HashMap;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import me.grishka.appkit.api.APIRequest;
|
import me.grishka.appkit.api.APIRequest;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
@@ -44,10 +47,11 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
TypeToken<T> respTypeToken;
|
TypeToken<T> respTypeToken;
|
||||||
Call okhttpCall;
|
Call okhttpCall;
|
||||||
Token token;
|
Token token;
|
||||||
boolean canceled;
|
boolean canceled, isRemote;
|
||||||
Map<String, String> headers;
|
Map<String, String> headers;
|
||||||
private ProgressDialog progressDialog;
|
private ProgressDialog progressDialog;
|
||||||
protected boolean removeUnsupportedItems;
|
protected boolean removeUnsupportedItems;
|
||||||
|
@Nullable Context context;
|
||||||
|
|
||||||
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
||||||
this.path=path;
|
this.path=path;
|
||||||
@@ -101,6 +105,21 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MastodonAPIRequest<T> execRemote(String domain) {
|
||||||
|
return execRemote(domain, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MastodonAPIRequest<T> execRemote(String domain, @Nullable AccountSession remoteSession) {
|
||||||
|
this.isRemote = true;
|
||||||
|
return Optional.ofNullable(remoteSession)
|
||||||
|
.or(() -> AccountSessionManager.getInstance().getLoggedInAccounts().stream()
|
||||||
|
.filter(acc -> acc.domain.equals(domain))
|
||||||
|
.findAny())
|
||||||
|
.map(AccountSession::getID)
|
||||||
|
.map(this::exec)
|
||||||
|
.orElse(this.execNoAuth(domain));
|
||||||
|
}
|
||||||
|
|
||||||
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
|
||||||
return wrapProgress(activity, message, cancelable, null);
|
return wrapProgress(activity, message, cancelable, null);
|
||||||
}
|
}
|
||||||
@@ -164,9 +183,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MastodonAPIRequest<T> setContext(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Context getContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
|
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
|
||||||
if(respObj instanceof BaseModel){
|
if(respObj instanceof BaseModel){
|
||||||
|
((BaseModel) respObj).isRemote = isRemote;
|
||||||
((BaseModel) respObj).postprocess();
|
((BaseModel) respObj).postprocess();
|
||||||
}else if(respObj instanceof List){
|
}else if(respObj instanceof List){
|
||||||
if(removeUnsupportedItems){
|
if(removeUnsupportedItems){
|
||||||
@@ -175,6 +205,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
Object item=itr.next();
|
Object item=itr.next();
|
||||||
if(item instanceof BaseModel){
|
if(item instanceof BaseModel){
|
||||||
try{
|
try{
|
||||||
|
((BaseModel) item).isRemote = isRemote;
|
||||||
((BaseModel) item).postprocess();
|
((BaseModel) item).postprocess();
|
||||||
}catch(ObjectValidationException x){
|
}catch(ObjectValidationException x){
|
||||||
Log.w(TAG, "Removing invalid object from list", x);
|
Log.w(TAG, "Removing invalid object from list", x);
|
||||||
@@ -182,15 +213,20 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// no idea why we're post-processing twice, but well, as long
|
||||||
|
// as upstream does it like this, i don't wanna break anything
|
||||||
for(Object item:((List<?>) respObj)){
|
for(Object item:((List<?>) respObj)){
|
||||||
if(item instanceof BaseModel){
|
if(item instanceof BaseModel){
|
||||||
|
((BaseModel) item).isRemote = isRemote;
|
||||||
((BaseModel) item).postprocess();
|
((BaseModel) item).postprocess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
for(Object item:((List<?>) respObj)){
|
for(Object item:((List<?>) respObj)){
|
||||||
if(item instanceof BaseModel)
|
if(item instanceof BaseModel) {
|
||||||
|
((BaseModel) item).isRemote = isRemote;
|
||||||
((BaseModel) item).postprocess();
|
((BaseModel) item).postprocess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class StatusInteractionController{
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result){
|
public void onSuccess(Status result){
|
||||||
runningFavoriteRequests.remove(status.id);
|
runningFavoriteRequests.remove(status.id);
|
||||||
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
|
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
|
||||||
cb.accept(result);
|
cb.accept(result);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ public class StatusInteractionController{
|
|||||||
public void onSuccess(Status reblog){
|
public void onSuccess(Status reblog){
|
||||||
Status result = reblog.getContentStatus();
|
Status result = reblog.getContentStatus();
|
||||||
runningReblogRequests.remove(status.id);
|
runningReblogRequests.remove(status.id);
|
||||||
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
|
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
|
||||||
cb.accept(result);
|
cb.accept(result);
|
||||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.joinmastodon.android.api.requests.accounts;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
|
||||||
|
public class GetAccountByHandle extends MastodonAPIRequest<Account>{
|
||||||
|
/**
|
||||||
|
* note that this method usually only returns a result if the instance already knows about an
|
||||||
|
* account - so it makes sense for looking up local users, search might be preferred otherwise
|
||||||
|
*/
|
||||||
|
public GetAccountByHandle(String acct){
|
||||||
|
super(HttpMethod.GET, "/accounts/lookup", Account.class);
|
||||||
|
addQueryParameter("acct", acct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,11 @@ public class AccountSessionManager{
|
|||||||
return sessions.get(id);
|
return sessions.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public AccountSession tryGetAccount(Account account) {
|
||||||
|
return sessions.get(account.getDomainFromURL() + "_" + account.id);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public AccountSession getLastActiveAccount(){
|
public AccountSession getLastActiveAccount(){
|
||||||
if(sessions.isEmpty() || lastActiveAccountID==null)
|
if(sessions.isEmpty() || lastActiveAccountID==null)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.parceler.Parcels;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
@@ -95,10 +96,11 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||||
return;
|
return;
|
||||||
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
}else if(filter==GetAccountStatuses.Filter.MEDIA){
|
||||||
if(ev.status.mediaAttachments.isEmpty())
|
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prependItems(Collections.singletonList(ev.status), true);
|
prependItems(Collections.singletonList(ev.status), true);
|
||||||
|
if (isOnTop()) scrollToTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||||
|
|||||||
@@ -94,12 +94,15 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
|||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Announcement> result){
|
public void onSuccess(List<Announcement> result){
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
List<Announcement> unread = result.stream().filter(a -> !a.read).collect(toList());
|
|
||||||
List<Announcement> read = result.stream().filter(a -> a.read).collect(toList());
|
// get unread items first
|
||||||
onDataLoaded(unread, true);
|
List<Announcement> data = result.stream().filter(a -> !a.read).collect(toList());
|
||||||
onDataLoaded(read, false);
|
if (data.isEmpty()) setResult(true, null);
|
||||||
if (unread.isEmpty()) setResult(true, null);
|
else unreadIDs = data.stream().map(a -> a.id).collect(toList());
|
||||||
else unreadIDs = unread.stream().map(a -> a.id).collect(toList());
|
|
||||||
|
// append read items at the end
|
||||||
|
data.addAll(result.stream().filter(a -> a.read).collect(toList()));
|
||||||
|
onDataLoaded(data, false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import android.text.TextPaint;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewTreeObserver;
|
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.animation.TranslateAnimation;
|
import android.view.animation.TranslateAnimation;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@@ -35,6 +34,7 @@ import org.joinmastodon.android.model.Relationship;
|
|||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||||
@@ -71,7 +71,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
import me.grishka.appkit.views.UsableRecyclerView;
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
|
||||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||||
protected DisplayItemsAdapter adapter;
|
protected DisplayItemsAdapter adapter;
|
||||||
protected String accountID;
|
protected String accountID;
|
||||||
@@ -82,6 +82,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||||
protected Rect tmpRect=new Rect();
|
protected Rect tmpRect=new Rect();
|
||||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
||||||
|
protected boolean currentlyScrolling;
|
||||||
|
|
||||||
public BaseStatusListFragment(){
|
public BaseStatusListFragment(){
|
||||||
super(20);
|
super(20);
|
||||||
@@ -95,7 +96,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
UiUtils.loadMaxWidth(getContext());
|
|
||||||
if(GlobalUserPreferences.disableMarquee){
|
if(GlobalUserPreferences.disableMarquee){
|
||||||
setTitleMarqueeEnabled(false);
|
setTitleMarqueeEnabled(false);
|
||||||
setSubtitleMarqueeEnabled(false);
|
setSubtitleMarqueeEnabled(false);
|
||||||
@@ -292,6 +292,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
fab.startAnimation(animate);
|
fab.startAnimation(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isScrolling() {
|
||||||
|
return currentlyScrolling;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hideFab() {
|
public void hideFab() {
|
||||||
View fab = getFab();
|
View fab = getFab();
|
||||||
@@ -319,7 +323,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
currentPhotoViewer.offsetView(-dx, -dy);
|
currentPhotoViewer.offsetView(-dx, -dy);
|
||||||
|
|
||||||
View fab = getFab();
|
View fab = getFab();
|
||||||
if (fab!=null && GlobalUserPreferences.autoHideFab) {
|
if (fab!=null && GlobalUserPreferences.autoHideFab && dy != UiUtils.SCROLL_TO_TOP_DELTA) {
|
||||||
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
|
||||||
hideFab();
|
hideFab();
|
||||||
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
|
||||||
@@ -332,6 +336,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState);
|
||||||
|
currentlyScrolling = newState != RecyclerView.SCROLL_STATE_IDLE;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
list.addItemDecoration(new StatusListItemDecoration());
|
list.addItemDecoration(new StatusListItemDecoration());
|
||||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||||
@@ -357,10 +367,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
if (firstIndex < 0) firstIndex = i;
|
if (firstIndex < 0) firstIndex = i;
|
||||||
lastIndex = i;
|
lastIndex = i;
|
||||||
StatusDisplayItem item = h.getItem();
|
StatusDisplayItem item = h.getItem();
|
||||||
hasDescendant = item.hasDescendantNeighbor();
|
hasDescendant = item.hasDescendantNeighbor;
|
||||||
// no for direct descendants because main status (right above) is
|
// no for direct descendants because main status (right above) is
|
||||||
// being displayed with an extended footer - no connected layout
|
// being displayed with an extended footer - no connected layout
|
||||||
hasAncestor = item.hasAncestoringNeighbor() && !item.isDirectDescendant;
|
hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant;
|
||||||
list.getDecoratedBoundsWithMargins(child, tmpRect);
|
list.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||||
outRect.left=Math.min(outRect.left, tmpRect.left);
|
outRect.left=Math.min(outRect.left, tmpRect.left);
|
||||||
outRect.top=Math.min(outRect.top, tmpRect.top);
|
outRect.top=Math.min(outRect.top, tmpRect.top);
|
||||||
@@ -375,7 +385,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
}
|
}
|
||||||
// shifting the selection box down
|
// shifting the selection box down
|
||||||
// see also: FooterStatusDisplayItem#onBind (setMargins)
|
// see also: FooterStatusDisplayItem#onBind (setMargins)
|
||||||
if (isWarning || firstIndex < 0 || lastIndex < 0) return;
|
if (isWarning || firstIndex < 0 || lastIndex < 0 ||
|
||||||
|
!(list.getChildViewHolder(list.getChildAt(lastIndex))
|
||||||
|
instanceof FooterStatusDisplayItem.Holder)) return;
|
||||||
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
|
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
|
||||||
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
|
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
|
||||||
list.getChildViewHolder(list.getChildAt(prevIndex))
|
list.getChildViewHolder(list.getChildAt(prevIndex))
|
||||||
@@ -668,6 +680,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
smoothScrollRecyclerViewToTop(list);
|
smoothScrollRecyclerViewToTop(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnTop() {
|
||||||
|
return isRecyclerViewOnTop(list);
|
||||||
|
}
|
||||||
|
|
||||||
protected int getListWidthForMediaLayout(){
|
protected int getListWidthForMediaLayout(){
|
||||||
return list.getWidth();
|
return list.getWidth();
|
||||||
}
|
}
|
||||||
@@ -737,6 +754,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDataLoaded(List<T> d, boolean more) {
|
||||||
|
super.onDataLoaded(d, more);
|
||||||
|
// more available, but the page isn't even full yet? seems wrong, let's load some more
|
||||||
|
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
|
||||||
|
}
|
||||||
|
|
||||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||||
|
|
||||||
public DisplayItemsAdapter(){
|
public DisplayItemsAdapter(){
|
||||||
@@ -797,7 +821,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||||||
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
|
||||||
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
|
||||||
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
|
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
|
||||||
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor()) continue;
|
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
|
||||||
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import static org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode.*;
|
||||||
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
|
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
|
||||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
|
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
|
||||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
|
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
|
||||||
@@ -149,7 +150,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
|
|||||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
|
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
||||||
|
|
||||||
private static final int MEDIA_RESULT=717;
|
private static final int MEDIA_RESULT=717;
|
||||||
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||||
@@ -355,6 +356,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
} else {
|
} else {
|
||||||
mediaBtn.setOnClickListener(v -> openFilePicker(false));
|
mediaBtn.setOnClickListener(v -> openFilePicker(false));
|
||||||
}
|
}
|
||||||
|
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
|
||||||
pollBtn.setOnClickListener(v->togglePoll());
|
pollBtn.setOnClickListener(v->togglePoll());
|
||||||
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
||||||
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
||||||
@@ -579,8 +581,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable s){
|
public void afterTextChanged(Editable s){
|
||||||
if(s.length()==0)
|
if(s.length()==0){
|
||||||
|
updateCharCounter();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
int start=lastChangeStart;
|
int start=lastChangeStart;
|
||||||
int count=lastChangeCount;
|
int count=lastChangeCount;
|
||||||
// offset one char back to catch an already typed '@' or '#' or ':'
|
// offset one char back to catch an already typed '@' or '#' or ':'
|
||||||
@@ -743,9 +747,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||||
hasSpoiler=true;
|
hasSpoiler=true;
|
||||||
spoilerEdit.setVisibility(View.VISIBLE);
|
spoilerEdit.setVisibility(View.VISIBLE);
|
||||||
if(GlobalUserPreferences.prefixRepliesWithRe && !status.spoilerText.startsWith("re: ")){
|
if ((GlobalUserPreferences.prefixReplies == ALWAYS
|
||||||
|
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
|
||||||
|
&& !status.spoilerText.startsWith("re: ")) {
|
||||||
spoilerEdit.setText("re: " + status.spoilerText);
|
spoilerEdit.setText("re: " + status.spoilerText);
|
||||||
}else{
|
} else {
|
||||||
spoilerEdit.setText(status.spoilerText);
|
spoilerEdit.setText(status.spoilerText);
|
||||||
}
|
}
|
||||||
spoilerBtn.setSelected(true);
|
spoilerBtn.setSelected(true);
|
||||||
@@ -845,12 +851,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
||||||
buildLanguageSelector(languageButton);
|
buildLanguageSelector(languageButton);
|
||||||
|
|
||||||
if (editingStatus != null && scheduledStatus == null) {
|
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||||
|
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
|
||||||
// editing an already published post
|
// editing an already published post
|
||||||
draftsBtn.setVisibility(View.GONE);
|
draftsBtn.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAccountID() {
|
||||||
|
return accountID;
|
||||||
|
}
|
||||||
|
|
||||||
private void navigateToUnsentPosts() {
|
private void navigateToUnsentPosts() {
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
@@ -1007,7 +1019,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
if(att.state!=AttachmentUploadState.DONE)
|
if(att.state!=AttachmentUploadState.DONE)
|
||||||
nonDoneAttachmentCount++;
|
nonDoneAttachmentCount++;
|
||||||
}
|
}
|
||||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
publishButton.setEnabled((!isInstancePixelfed() || attachments.size() > 0) && (trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||||
sendError.setVisibility(View.GONE);
|
sendError.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1149,7 +1161,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
sendProgress.setVisibility(View.VISIBLE);
|
sendProgress.setVisibility(View.VISIBLE);
|
||||||
sendError.setVisibility(View.GONE);
|
sendError.setVisibility(View.GONE);
|
||||||
|
|
||||||
Callback<Status> resCallback=new Callback<>(){
|
Callback<Status> resCallback = new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result){
|
public void onSuccess(Status result){
|
||||||
maybeDeleteScheduledPost(() -> {
|
maybeDeleteScheduledPost(() -> {
|
||||||
@@ -1162,7 +1174,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
E.post(new StatusUpdatedEvent(result));
|
// pixelfed doesn't return the edited status :/
|
||||||
|
Status editedStatus = result == null ? editingStatus : result;
|
||||||
|
if (result == null) {
|
||||||
|
editedStatus.text = req.status;
|
||||||
|
editedStatus.spoilerText = req.spoilerText;
|
||||||
|
editedStatus.sensitive = req.sensitive;
|
||||||
|
editedStatus.language = req.language;
|
||||||
|
// user will have to reload to see html
|
||||||
|
editedStatus.content = req.status;
|
||||||
|
}
|
||||||
|
E.post(new StatusUpdatedEvent(editedStatus));
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
||||||
Nav.finish(ComposeFragment.this);
|
Nav.finish(ComposeFragment.this);
|
||||||
@@ -1897,9 +1919,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||||||
visibilityPopup=new PopupMenu(getActivity(), v);
|
visibilityPopup=new PopupMenu(getActivity(), v);
|
||||||
visibilityPopup.inflate(R.menu.compose_visibility);
|
visibilityPopup.inflate(R.menu.compose_visibility);
|
||||||
Menu m=visibilityPopup.getMenu();
|
Menu m=visibilityPopup.getMenu();
|
||||||
|
if (isInstancePixelfed()) {
|
||||||
|
m.findItem(R.id.vis_private).setVisible(false);
|
||||||
|
}
|
||||||
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
|
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
|
||||||
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
||||||
if (instance.isAkkoma()) {
|
if (isInstanceAkkoma()) {
|
||||||
m.findItem(R.id.vis_local).setVisible(true);
|
m.findItem(R.id.vis_local).setVisible(true);
|
||||||
} else if (localOnly || prefsSaysSupported) {
|
} else if (localOnly || prefsSaysSupported) {
|
||||||
localOnlyItem.setVisible(true);
|
localOnlyItem.setVisible(true);
|
||||||
|
|||||||
@@ -255,6 +255,10 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
|
|||||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||||
|
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
relationship=relationships.get(item.account.id);
|
relationship=relationships.get(item.account.id);
|
||||||
if(relationship == null || !relationship.followedBy){
|
if(relationship == null || !relationship.followedBy){
|
||||||
actionWrap.setVisibility(View.GONE);
|
actionWrap.setVisibility(View.GONE);
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ public interface HasAccountID {
|
|||||||
return getInstance().map(Instance::isAkkoma).orElse(false);
|
return getInstance().map(Instance::isAkkoma).orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean isInstancePixelfed() {
|
||||||
|
return getInstance().map(Instance::isPixelfed).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
default Optional<Instance> getInstance() {
|
default Optional<Instance> getInstance() {
|
||||||
return getSession().getInstance();
|
return getSession().getInstance();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ public interface HasFab {
|
|||||||
View getFab();
|
View getFab();
|
||||||
void showFab();
|
void showFab();
|
||||||
void hideFab();
|
void hideFab();
|
||||||
|
boolean isScrolling();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||||||
}
|
}
|
||||||
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
|
||||||
maybeTriggerLoading(newFragment);
|
maybeTriggerLoading(newFragment);
|
||||||
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
|
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
|
||||||
currentTab=tab;
|
currentTab=tab;
|
||||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||||
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
|
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
|
||||||
|
|||||||
@@ -460,6 +460,12 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
|||||||
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.hideFab();
|
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) l.hideFab();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isScrolling() {
|
||||||
|
return (fragments[pager.getCurrentItem()] instanceof HasFab fabulous)
|
||||||
|
&& fabulous.isScrolling();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSwitcherIcon(int i) {
|
private void updateSwitcherIcon(int i) {
|
||||||
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
|
||||||
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
timelineTitle.setText(timelines[i].getTitle(getContext()));
|
||||||
|
|||||||
@@ -48,11 +48,13 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean typeFilterPredicate(Status s) {
|
||||||
|
return (GlobalUserPreferences.showReplies || s.inReplyToId == null) &&
|
||||||
|
(GlobalUserPreferences.showBoosts || s.reblog == null);
|
||||||
|
}
|
||||||
|
|
||||||
private List<Status> filterPosts(List<Status> items) {
|
private List<Status> filterPosts(List<Status> items) {
|
||||||
return items.stream().filter(i ->
|
return items.stream().filter(this::typeFilterPredicate).collect(Collectors.toList());
|
||||||
(GlobalUserPreferences.showReplies || i.inReplyToId == null) &&
|
|
||||||
(GlobalUserPreferences.showBoosts || i.reblog == null)
|
|
||||||
).collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -232,7 +234,7 @@ public class HomeTimelineFragment extends StatusListFragment {
|
|||||||
for(Status s:result){
|
for(Status s:result){
|
||||||
if(idsBelowGap.contains(s.id))
|
if(idsBelowGap.contains(s.id))
|
||||||
break;
|
break;
|
||||||
if(filterPredicate.test(s)){
|
if(typeFilterPredicate(s) && filterPredicate.test(s)){
|
||||||
targetList.addAll(buildDisplayItems(s));
|
targetList.addAll(buildDisplayItems(s));
|
||||||
insertedPosts.add(s);
|
insertedPosts.add(s);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,9 @@ public abstract class MastodonToolbarFragment extends ToolbarFragment{
|
|||||||
toolbar.setNavigationContentDescription(R.string.back);
|
toolbar.setNavigationContentDescription(R.string.back);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean wantsToolbarMenuIconsTinted() {
|
||||||
|
return false; // else, badged icons don't work :(
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
|||||||
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Emoji;
|
import org.joinmastodon.android.model.Emoji;
|
||||||
@@ -25,6 +26,8 @@ import org.joinmastodon.android.model.Markers;
|
|||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||||
@@ -192,23 +195,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
@Override
|
@Override
|
||||||
public void onItemClick(String id){
|
public void onItemClick(String id){
|
||||||
Notification n=getNotificationByID(id);
|
Notification n=getNotificationByID(id);
|
||||||
if(n.status!=null){
|
Bundle args = new Bundle();
|
||||||
Status status=n.status;
|
if(n.status != null && n.status.inReplyToAccountId != null && knownAccounts.containsKey(n.status.inReplyToAccountId))
|
||||||
Bundle args=new Bundle();
|
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(n.status.inReplyToAccountId)));
|
||||||
args.putString("account", accountID);
|
UiUtils.showFragmentForNotification(getContext(), n, accountID, args);
|
||||||
args.putParcelable("status", Parcels.wrap(status));
|
|
||||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
|
||||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
|
||||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
|
||||||
}else if(n.report != null){
|
|
||||||
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
|
|
||||||
UiUtils.launchWebBrowser(getActivity(), "https://"+domain+"/admin/reports/"+n.report.id);
|
|
||||||
}else{
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
|
||||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -240,6 +230,32 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copied from StatusListFragment.EventListener (just like the method above)
|
||||||
|
// (which assumes this.data to be a list of statuses...)
|
||||||
|
@Subscribe
|
||||||
|
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
|
||||||
|
for(Notification n:data){
|
||||||
|
if (n.status == null) continue;
|
||||||
|
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||||
|
n.status.getContentStatus().update(ev);
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||||
|
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||||
|
footer.rebind();
|
||||||
|
}else if(holder instanceof ExtendedFooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
|
||||||
|
footer.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(Notification n:preloadedData){
|
||||||
|
if (n.status == null) continue;
|
||||||
|
if(n.status.getContentStatus().id.equals(ev.id)){
|
||||||
|
n.status.getContentStatus().update(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||||
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Filter;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
|
public class PinnedPostsListFragment extends StatusListFragment{
|
||||||
|
private Account account;
|
||||||
|
|
||||||
|
public PinnedPostsListFragment() {
|
||||||
|
setListLayoutId(R.layout.recycler_fragment_no_refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||||
|
setTitle(R.string.posts);
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
new GetAccountStatuses(account.id, null, null, 100, GetAccountStatuses.Filter.PINNED)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Status> result){
|
||||||
|
onDataLoaded(result, false);
|
||||||
|
}
|
||||||
|
}).exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Filter.FilterContext getFilterContext() {
|
||||||
|
return Filter.FilterContext.ACCOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
|
return Uri.parse(account.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,7 @@ import android.widget.Toolbar;
|
|||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||||
@@ -58,7 +59,6 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
|||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.AccountField;
|
import org.joinmastodon.android.model.AccountField;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
import org.joinmastodon.android.model.Instance;
|
|
||||||
import org.joinmastodon.android.model.Relationship;
|
import org.joinmastodon.android.model.Relationship;
|
||||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||||
@@ -124,7 +124,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
private ProgressBarButton actionButton, notifyButton;
|
private ProgressBarButton actionButton, notifyButton;
|
||||||
private ViewPager2 pager;
|
private ViewPager2 pager;
|
||||||
private NestedRecyclerScrollView scrollView;
|
private NestedRecyclerScrollView scrollView;
|
||||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
|
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
|
||||||
|
private PinnedPostsListFragment pinnedPostsFragment;
|
||||||
// private ProfileAboutFragment aboutFragment;
|
// private ProfileAboutFragment aboutFragment;
|
||||||
private TabLayout tabbar;
|
private TabLayout tabbar;
|
||||||
private SwipeRefreshLayout refreshLayout;
|
private SwipeRefreshLayout refreshLayout;
|
||||||
@@ -138,7 +139,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
private TextView followsYouView;
|
private TextView followsYouView;
|
||||||
private ViewGroup rolesView;
|
private ViewGroup rolesView;
|
||||||
|
|
||||||
private Account account;
|
private Account account, remoteAccount;
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private String domain;
|
private String domain;
|
||||||
private Relationship relationship;
|
private Relationship relationship;
|
||||||
@@ -177,7 +178,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
|
|
||||||
accountID=getArguments().getString("account");
|
accountID=getArguments().getString("account");
|
||||||
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
|
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||||
if(getArguments().containsKey("profileAccount")){
|
if (getArguments().containsKey("remoteAccount")) {
|
||||||
|
remoteAccount = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
|
||||||
|
if(!getArguments().getBoolean("noAutoLoad", false))
|
||||||
|
loadData();
|
||||||
|
} else if(getArguments().containsKey("profileAccount")){
|
||||||
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||||
profileAccountID=account.id;
|
profileAccountID=account.id;
|
||||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||||
@@ -348,36 +353,55 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return sizeWrapper;
|
return sizeWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onAccountLoaded(Account result) {
|
||||||
|
account=result;
|
||||||
|
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||||
|
bindHeaderView();
|
||||||
|
dataLoaded();
|
||||||
|
if(!tabLayoutMediator.isAttached())
|
||||||
|
tabLayoutMediator.attach();
|
||||||
|
if(!isOwnProfile)
|
||||||
|
loadRelationship();
|
||||||
|
else
|
||||||
|
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||||
|
if(refreshing){
|
||||||
|
refreshing=false;
|
||||||
|
refreshLayout.setRefreshing(false);
|
||||||
|
if(postsFragment.loaded)
|
||||||
|
postsFragment.onRefresh();
|
||||||
|
if(postsWithRepliesFragment.loaded)
|
||||||
|
postsWithRepliesFragment.onRefresh();
|
||||||
|
if(pinnedPostsFragment.loaded)
|
||||||
|
pinnedPostsFragment.onRefresh();
|
||||||
|
if(mediaFragment.loaded)
|
||||||
|
mediaFragment.onRefresh();
|
||||||
|
}
|
||||||
|
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(){
|
protected void doLoadData(){
|
||||||
|
if (remoteAccount != null) {
|
||||||
|
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
|
||||||
|
if (getContext() == null) return;
|
||||||
|
if (args == null || !args.containsKey("profileAccount")) {
|
||||||
|
Toast.makeText(getContext(), getContext().getString(
|
||||||
|
R.string.sk_error_loading_profile, domain
|
||||||
|
), Toast.LENGTH_SHORT).show();
|
||||||
|
Nav.finish(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onAccountLoaded(Parcels.unwrap(args.getParcelable("profileAccount")));
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
currentRequest=new GetAccountByID(profileAccountID)
|
currentRequest=new GetAccountByID(profileAccountID)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Account result){
|
public void onSuccess(Account result){
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
account=result;
|
onAccountLoaded(result);
|
||||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
|
||||||
bindHeaderView();
|
|
||||||
dataLoaded();
|
|
||||||
if(!tabLayoutMediator.isAttached())
|
|
||||||
tabLayoutMediator.attach();
|
|
||||||
if(!isOwnProfile)
|
|
||||||
loadRelationship();
|
|
||||||
else
|
|
||||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
|
||||||
if(refreshing){
|
|
||||||
refreshing=false;
|
|
||||||
refreshLayout.setRefreshing(false);
|
|
||||||
if(postsFragment.loaded)
|
|
||||||
postsFragment.onRefresh();
|
|
||||||
if(postsWithRepliesFragment.loaded)
|
|
||||||
postsWithRepliesFragment.onRefresh();
|
|
||||||
if(pinnedPostsFragment.loaded)
|
|
||||||
pinnedPostsFragment.onRefresh();
|
|
||||||
if(mediaFragment.loaded)
|
|
||||||
mediaFragment.onRefresh();
|
|
||||||
}
|
|
||||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
@@ -398,8 +422,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if(postsFragment==null){
|
if(postsFragment==null){
|
||||||
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
|
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
|
||||||
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
||||||
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
|
|
||||||
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
||||||
|
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||||
|
args.putBoolean("__is_tab", true);
|
||||||
|
pinnedPostsFragment=new PinnedPostsListFragment();
|
||||||
|
pinnedPostsFragment.setArguments(args);
|
||||||
// aboutFragment=new ProfileAboutFragment();
|
// aboutFragment=new ProfileAboutFragment();
|
||||||
setFields(fields);
|
setFields(fields);
|
||||||
}
|
}
|
||||||
@@ -491,9 +521,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||||
name.setText(ssb);
|
name.setText(ssb);
|
||||||
setTitle(ssb);
|
setTitle(ssb);
|
||||||
|
|
||||||
if (account.roles != null && !account.roles.isEmpty()) {
|
if (account.roles != null && !account.roles.isEmpty()) {
|
||||||
rolesView.setVisibility(View.VISIBLE);
|
rolesView.setVisibility(View.VISIBLE);
|
||||||
@@ -512,13 +542,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
|
|
||||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||||
|
|
||||||
|
String acct = ((isSelf || account.isRemote)
|
||||||
|
? account.getFullyQualifiedName()
|
||||||
|
: account.acct);
|
||||||
if(account.locked){
|
if(account.locked){
|
||||||
ssb=new SpannableStringBuilder("@");
|
ssb=new SpannableStringBuilder("@");
|
||||||
ssb.append(account.acct);
|
ssb.append(acct);
|
||||||
if(isSelf){
|
|
||||||
ssb.append('@');
|
|
||||||
ssb.append(domain);
|
|
||||||
}
|
|
||||||
ssb.append(" ");
|
ssb.append(" ");
|
||||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
|
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
|
||||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||||
@@ -527,7 +556,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
username.setText(ssb);
|
username.setText(ssb);
|
||||||
}else{
|
}else{
|
||||||
// noinspection SetTextI18n
|
// noinspection SetTextI18n
|
||||||
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : ""));
|
username.setText('@'+acct);
|
||||||
}
|
}
|
||||||
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)){
|
||||||
@@ -543,6 +572,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
|
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
|
||||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, account.statusesCount)));
|
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, account.statusesCount)));
|
||||||
|
|
||||||
|
if (account.followersCount < 0) followersBtn.setVisibility(View.GONE);
|
||||||
|
if (account.followingCount < 0) followingBtn.setVisibility(View.GONE);
|
||||||
|
|
||||||
UiUtils.loadCustomEmojiInTextView(name);
|
UiUtils.loadCustomEmojiInTextView(name);
|
||||||
UiUtils.loadCustomEmojiInTextView(bio);
|
UiUtils.loadCustomEmojiInTextView(bio);
|
||||||
|
|
||||||
@@ -597,6 +629,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean wantsToolbarMenuIconsTinted() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
if(isOwnProfile && isInEditMode){
|
if(isOwnProfile && isInEditMode){
|
||||||
@@ -627,8 +664,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
|
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
|
||||||
if(isOwnProfile)
|
if(isOwnProfile) {
|
||||||
|
if (isInstancePixelfed()) menu.findItem(R.id.scheduled).setVisible(false);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MenuItem mute = menu.findItem(R.id.mute);
|
MenuItem mute = menu.findItem(R.id.mute);
|
||||||
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||||
@@ -775,6 +814,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||||||
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.hideFab();
|
if (getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous) fabulous.hideFab();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isScrolling() {
|
||||||
|
return getFragmentForPage(pager.getCurrentItem()) instanceof HasFab fabulous
|
||||||
|
&& fabulous.isScrolling();
|
||||||
|
}
|
||||||
|
|
||||||
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){
|
||||||
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
int topBarsH=getToolbar().getHeight()+statusBarHeight;
|
||||||
if(scrollY>avatarBorder.getTop()-topBarsH){
|
if(scrollY>avatarBorder.getTop()-topBarsH){
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package org.joinmastodon.android.fragments;
|
|||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.utils.V;
|
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
public interface ScrollableToTop{
|
public interface ScrollableToTop{
|
||||||
void scrollToTop();
|
void scrollToTop();
|
||||||
@@ -21,7 +22,7 @@ public interface ScrollableToTop{
|
|||||||
@Override
|
@Override
|
||||||
public boolean onPreDraw(){
|
public boolean onPreDraw(){
|
||||||
list.getViewTreeObserver().removeOnPreDrawListener(this);
|
list.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
list.scrollBy(0, V.dp(300));
|
list.scrollBy(0, UiUtils.SCROLL_TO_TOP_DELTA);
|
||||||
list.smoothScrollToPosition(0);
|
list.smoothScrollToPosition(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ import com.squareup.otto.Subscribe;
|
|||||||
import org.joinmastodon.android.BuildConfig;
|
import org.joinmastodon.android.BuildConfig;
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
|
||||||
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
|
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode;
|
||||||
import org.joinmastodon.android.MainActivity;
|
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;
|
||||||
@@ -85,8 +87,8 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
private ArrayList<Item> items=new ArrayList<>();
|
private ArrayList<Item> items=new ArrayList<>();
|
||||||
private ThemeItem themeItem;
|
private ThemeItem themeItem;
|
||||||
private NotificationPolicyItem notificationPolicyItem;
|
private NotificationPolicyItem notificationPolicyItem;
|
||||||
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
|
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
|
||||||
private ButtonItem defaultContentTypeButtonItem;
|
private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
|
||||||
private String accountID;
|
private String accountID;
|
||||||
private boolean needUpdateNotificationSettings;
|
private boolean needUpdateNotificationSettings;
|
||||||
private boolean needAppRestart;
|
private boolean needAppRestart;
|
||||||
@@ -189,9 +191,18 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
GlobalUserPreferences.showInteractionCounts=i.checked;
|
GlobalUserPreferences.showInteractionCounts=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
|
items.add(alwaysRevealSpoilersItem = 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();
|
||||||
|
if (list.findViewHolderForAdapterPosition(items.indexOf(autoRevealSpoilersItem)) instanceof ButtonViewHolder bvh) bvh.rebind();
|
||||||
|
}));
|
||||||
|
items.add(autoRevealSpoilersItem = new ButtonItem(R.string.sk_settings_auto_reveal_equal_spoilers, R.drawable.ic_fluent_eye_24_regular, b->{
|
||||||
|
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||||
|
popupMenu.inflate(R.menu.settings_auto_reveal_spoiler);
|
||||||
|
popupMenu.setOnMenuItemClickListener(i -> onAutoRevealSpoilerClick(i, b));
|
||||||
|
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||||
|
b.setOnClickListener(v->popupMenu.show());
|
||||||
|
onAutoRevealSpoilerChanged(b);
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
|
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
|
||||||
GlobalUserPreferences.disableSwipe=i.checked;
|
GlobalUserPreferences.disableSwipe=i.checked;
|
||||||
@@ -211,14 +222,32 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
|
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.prefixRepliesWithRe, i->{
|
items.add(new ButtonItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, b->{
|
||||||
GlobalUserPreferences.prefixRepliesWithRe=i.checked;
|
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
|
||||||
|
popupMenu.inflate(R.menu.settings_prefix_reply_mode);
|
||||||
|
popupMenu.setOnMenuItemClickListener(i -> onPrefixRepliesClick(i, b));
|
||||||
|
b.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||||
|
b.setOnClickListener(v->popupMenu.show());
|
||||||
|
b.setText(switch(GlobalUserPreferences.prefixReplies){
|
||||||
|
case TO_OTHERS -> R.string.sk_settings_prefix_replies_to_others;
|
||||||
|
case ALWAYS -> R.string.sk_settings_prefix_replies_always;
|
||||||
|
default -> R.string.sk_settings_prefix_replies_never;
|
||||||
|
});
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_settings_confirm_before_reblog, R.drawable.ic_fluent_checkmark_circle_24_regular, GlobalUserPreferences.confirmBeforeReblog, i->{
|
items.add(new SwitchItem(R.string.sk_settings_confirm_before_reblog, R.drawable.ic_fluent_checkmark_circle_24_regular, GlobalUserPreferences.confirmBeforeReblog, i->{
|
||||||
GlobalUserPreferences.confirmBeforeReblog=i.checked;
|
GlobalUserPreferences.confirmBeforeReblog=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
|
items.add(new SwitchItem(R.string.sk_settings_forward_report_default, R.drawable.ic_fluent_arrow_forward_24_regular, GlobalUserPreferences.forwardReportDefault, i->{
|
||||||
|
GlobalUserPreferences.forwardReportDefault=i.checked;
|
||||||
|
GlobalUserPreferences.save();
|
||||||
|
}));
|
||||||
|
items.add(new SwitchItem(R.string.sk_settings_allow_remote_loading, R.drawable.ic_fluent_communication_24_regular, GlobalUserPreferences.allowRemoteLoading, i->{
|
||||||
|
GlobalUserPreferences.allowRemoteLoading=i.checked;
|
||||||
|
GlobalUserPreferences.save();
|
||||||
|
}));
|
||||||
|
items.add(new SmallTextItem(R.string.sk_settings_allow_remote_loading_explanation));
|
||||||
|
|
||||||
items.add(new HeaderItem(R.string.sk_timelines));
|
items.add(new HeaderItem(R.string.sk_timelines));
|
||||||
items.add(new SwitchItem(R.string.sk_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->{
|
||||||
@@ -271,7 +300,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
GlobalUserPreferences.collapseLongPosts=i.checked;
|
GlobalUserPreferences.collapseLongPosts=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
}));
|
}));
|
||||||
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{
|
items.add(new SwitchItem(R.string.sk_settings_hide_interaction, R.drawable.ic_fluent_star_off_24_regular, GlobalUserPreferences.spectatorMode, i->{
|
||||||
GlobalUserPreferences.spectatorMode=i.checked;
|
GlobalUserPreferences.spectatorMode=i.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
needAppRestart=true;
|
needAppRestart=true;
|
||||||
@@ -526,6 +555,52 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean onPrefixRepliesClick(MenuItem item, Button btn) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
PrefixRepliesMode mode = PrefixRepliesMode.NEVER;
|
||||||
|
if (id == R.id.prefix_replies_always) mode = PrefixRepliesMode.ALWAYS;
|
||||||
|
else if (id == R.id.prefix_replies_to_others) mode = PrefixRepliesMode.TO_OTHERS;
|
||||||
|
GlobalUserPreferences.prefixReplies = mode;
|
||||||
|
|
||||||
|
btn.setText(switch(GlobalUserPreferences.prefixReplies){
|
||||||
|
case TO_OTHERS -> R.string.sk_settings_prefix_replies_to_others;
|
||||||
|
case ALWAYS -> R.string.sk_settings_prefix_replies_always;
|
||||||
|
default -> R.string.sk_settings_prefix_replies_never;
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean onAutoRevealSpoilerClick(MenuItem item, Button btn) {
|
||||||
|
int id = item.getItemId();
|
||||||
|
|
||||||
|
AutoRevealMode mode = AutoRevealMode.NEVER;
|
||||||
|
if (id == R.id.auto_reveal_threads) mode = AutoRevealMode.THREADS;
|
||||||
|
else if (id == R.id.auto_reveal_discussions) mode = AutoRevealMode.DISCUSSIONS;
|
||||||
|
|
||||||
|
GlobalUserPreferences.alwaysExpandContentWarnings = false;
|
||||||
|
GlobalUserPreferences.autoRevealEqualSpoilers = mode;
|
||||||
|
GlobalUserPreferences.save();
|
||||||
|
onAutoRevealSpoilerChanged(btn);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAutoRevealSpoilerChanged(Button b) {
|
||||||
|
if (GlobalUserPreferences.alwaysExpandContentWarnings) {
|
||||||
|
b.setText(R.string.sk_settings_auto_reveal_anyone);
|
||||||
|
} else {
|
||||||
|
b.setText(switch(GlobalUserPreferences.autoRevealEqualSpoilers){
|
||||||
|
case THREADS -> R.string.sk_settings_auto_reveal_author;
|
||||||
|
case DISCUSSIONS -> R.string.sk_settings_auto_reveal_anyone;
|
||||||
|
default -> R.string.sk_settings_auto_reveal_nobody;
|
||||||
|
});
|
||||||
|
if (alwaysRevealSpoilersItem.checked != GlobalUserPreferences.alwaysExpandContentWarnings) {
|
||||||
|
alwaysRevealSpoilersItem.checked = GlobalUserPreferences.alwaysExpandContentWarnings;
|
||||||
|
if (list.findViewHolderForAdapterPosition(items.indexOf(alwaysRevealSpoilersItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void onTrueBlackThemeChanged(SwitchItem item){
|
private void onTrueBlackThemeChanged(SwitchItem item){
|
||||||
GlobalUserPreferences.trueBlackTheme=item.checked;
|
GlobalUserPreferences.trueBlackTheme=item.checked;
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
@@ -555,14 +630,14 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
|
|
||||||
private boolean onContentTypeChanged(MenuItem item, Button btn){
|
private boolean onContentTypeChanged(MenuItem item, Button btn){
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
ContentType contentType = switch (id) {
|
|
||||||
case R.id.content_type_plain -> ContentType.PLAIN;
|
ContentType contentType = null;
|
||||||
case R.id.content_type_html -> ContentType.HTML;
|
if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
|
||||||
case R.id.content_type_markdown -> ContentType.MARKDOWN;
|
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
|
||||||
case R.id.content_type_bbcode -> ContentType.BBCODE;
|
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
|
||||||
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN;
|
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
|
||||||
default -> null;
|
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
|
||||||
};
|
|
||||||
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
|
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
|
||||||
GlobalUserPreferences.save();
|
GlobalUserPreferences.save();
|
||||||
btn.setText(getContentTypeString(contentType));
|
btn.setText(getContentTypeString(contentType));
|
||||||
@@ -835,7 +910,11 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class SmallTextItem extends Item {
|
private class SmallTextItem extends Item {
|
||||||
private String text;
|
private final String text;
|
||||||
|
|
||||||
|
public SmallTextItem(@StringRes int text) {
|
||||||
|
this.text = getString(text);
|
||||||
|
}
|
||||||
|
|
||||||
public SmallTextItem(String text) {
|
public SmallTextItem(String text) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
@@ -1076,7 +1155,6 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
private final ImageView icon;
|
private final ImageView icon;
|
||||||
private final TextView text;
|
private final TextView text;
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
public ButtonViewHolder(){
|
public ButtonViewHolder(){
|
||||||
super(getActivity(), R.layout.item_settings_button, list);
|
super(getActivity(), R.layout.item_settings_button, list);
|
||||||
text=findViewById(R.id.text);
|
text=findViewById(R.id.text);
|
||||||
@@ -1084,14 +1162,17 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
|
|||||||
button=findViewById(R.id.button);
|
button=findViewById(R.id.button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public void onBind(ButtonItem item){
|
public void onBind(ButtonItem item){
|
||||||
text.setText(item.text);
|
text.setText(item.text);
|
||||||
if (item.icon == 0) {
|
icon.setVisibility(item.icon == 0 ? View.GONE : View.VISIBLE);
|
||||||
icon.setVisibility(View.GONE);
|
icon.setImageResource(item.icon == 0 ? 0 : item.icon);
|
||||||
} else {
|
// reset listeners before letting the button consumer consume the button
|
||||||
icon.setImageResource(item.icon);
|
// (and potentially set some listeners, but not others)
|
||||||
}
|
button.setOnTouchListener(null);
|
||||||
|
button.setOnClickListener(null);
|
||||||
|
button.setOnLongClickListener(null);
|
||||||
item.buttonConsumer.accept(button);
|
item.buttonConsumer.accept(button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
|||||||
status.filterRevealed = true;
|
status.filterRevealed = true;
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putParcelable("status", Parcels.wrap(status));
|
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||||
@@ -164,13 +164,29 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
|||||||
protected void removeStatus(Status status){
|
protected void removeStatus(Status status){
|
||||||
data.remove(status);
|
data.remove(status);
|
||||||
preloadedData.remove(status);
|
preloadedData.remove(status);
|
||||||
int index=-1;
|
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
|
||||||
for(int i=0;i<displayItems.size();i++){
|
for(int i=0;i<displayItems.size();i++){
|
||||||
if(status.id.equals(displayItems.get(i).parentID)){
|
StatusDisplayItem item = displayItems.get(i);
|
||||||
|
if(status.id.equals(item.parentID)){
|
||||||
index=i;
|
index=i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (item.parentID.equals(status.inReplyToId)) {
|
||||||
|
if (ancestorFirstIndex == -1) ancestorFirstIndex = i;
|
||||||
|
ancestorLastIndex = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// did we find an ancestor that is also the status' neighbor?
|
||||||
|
if (ancestorFirstIndex >= 0 && ancestorLastIndex == index - 1) {
|
||||||
|
for (int i = ancestorFirstIndex; i <= ancestorLastIndex; i++) {
|
||||||
|
StatusDisplayItem item = displayItems.get(i);
|
||||||
|
// update ancestor to have no descendant anymore
|
||||||
|
if (item.parentID.equals(status.inReplyToId)) item.hasDescendantNeighbor = false;
|
||||||
|
}
|
||||||
|
adapter.notifyItemRangeChanged(ancestorFirstIndex, ancestorLastIndex - ancestorFirstIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
if(index==-1)
|
if(index==-1)
|
||||||
return;
|
return;
|
||||||
int lastIndex;
|
int lastIndex;
|
||||||
|
|||||||
@@ -2,18 +2,25 @@ package org.joinmastodon.android.fragments;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
||||||
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
|
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.model.StatusContext;
|
import org.joinmastodon.android.model.StatusContext;
|
||||||
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||||
@@ -36,31 +43,17 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||||
protected Status mainStatus;
|
protected Status mainStatus, updatedStatus;
|
||||||
|
|
||||||
/**
|
|
||||||
* lists the hierarchy of ancestors and descendants in a thread. level 0 = the main status.
|
|
||||||
* e.g.
|
|
||||||
* <pre>
|
|
||||||
* [0] ancestor: -2 ↰
|
|
||||||
* [1] ancestor: -1 ↰
|
|
||||||
* [2] main status: 0 ↰
|
|
||||||
* [3] descendant: 1 ↰
|
|
||||||
* [4] descendant: 2 ↰
|
|
||||||
* [5] descendant: 3
|
|
||||||
* [6] descendant: 1
|
|
||||||
* [7] descendant: 1 ↰
|
|
||||||
* [8] descendant: 2
|
|
||||||
* </pre>
|
|
||||||
* confused? good. /j
|
|
||||||
*/
|
|
||||||
private final List<Pair<String, Integer>> levels = new ArrayList<>();
|
|
||||||
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||||
|
private StatusContext result;
|
||||||
|
protected boolean contextInitiallyRendered, transitionFinished;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -92,15 +85,17 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
|
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
|
||||||
if (ancestryInfo != null) {
|
if (ancestryInfo != null) {
|
||||||
item.setAncestryInfo(
|
item.setAncestryInfo(
|
||||||
ancestryInfo,
|
ancestryInfo.descendantNeighbor != null,
|
||||||
|
ancestryInfo.ancestoringNeighbor != null,
|
||||||
s.id.equals(mainStatus.id),
|
s.id.equals(mainStatus.id),
|
||||||
ancestryInfo.getAncestoringNeighbor()
|
Optional.ofNullable(ancestryInfo.ancestoringNeighbor)
|
||||||
.map(ancestor -> ancestor.id.equals(mainStatus.id))
|
.map(ancestor -> ancestor.id.equals(mainStatus.id))
|
||||||
.orElse(false)
|
.orElse(false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item instanceof ReblogOrReplyLineStatusDisplayItem && !item.isDirectDescendant) {
|
if (item instanceof ReblogOrReplyLineStatusDisplayItem &&
|
||||||
|
(!item.isDirectDescendant && item.hasAncestoringNeighbor)) {
|
||||||
deleteTheseItems.add(i);
|
deleteTheseItems.add(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,63 +106,145 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
footer.hideCounts=true;
|
footer.hideCounts=true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
||||||
if(s.id.equals(mainStatus.id)) {
|
if(s.id.equals(mainStatus.id)) {
|
||||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
|
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransitionFinished() {
|
||||||
|
transitionFinished = true;
|
||||||
|
maybeApplyContext();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
|
if (refreshing) loadMainStatus();
|
||||||
currentRequest=new GetStatusContext(mainStatus.id)
|
currentRequest=new GetStatusContext(mainStatus.id)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(StatusContext result){
|
public void onSuccess(StatusContext result){
|
||||||
if (getActivity() == null) return;
|
ThreadFragment.this.result = result;
|
||||||
if(refreshing){
|
maybeApplyContext();
|
||||||
data.clear();
|
|
||||||
ancestryMap.clear();
|
|
||||||
displayItems.clear();
|
|
||||||
data.add(mainStatus);
|
|
||||||
onAppendItems(Collections.singletonList(mainStatus));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: figure out how this code works
|
|
||||||
if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
|
|
||||||
|
|
||||||
result.descendants=filterStatuses(result.descendants);
|
|
||||||
result.ancestors=filterStatuses(result.ancestors);
|
|
||||||
|
|
||||||
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
|
|
||||||
ancestryMap.put(i.status.id, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(footerProgress!=null)
|
|
||||||
footerProgress.setVisibility(View.GONE);
|
|
||||||
data.addAll(result.descendants);
|
|
||||||
int prevCount=displayItems.size();
|
|
||||||
onAppendItems(result.descendants);
|
|
||||||
int count=displayItems.size();
|
|
||||||
if(!refreshing)
|
|
||||||
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
|
|
||||||
int prependedCount = prependItems(result.ancestors, !refreshing);
|
|
||||||
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
|
|
||||||
displayItems.remove(prependedCount);
|
|
||||||
adapter.notifyItemRemoved(prependedCount);
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
dataLoaded();
|
|
||||||
if(refreshing){
|
|
||||||
refreshDone();
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
list.scrollToPosition(displayItems.size()-count);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadMainStatus() {
|
||||||
|
new GetStatusByID(mainStatus.id)
|
||||||
|
.setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Status status) {
|
||||||
|
if (getContext() == null || status == null) return;
|
||||||
|
updatedStatus = status;
|
||||||
|
// for the case that the context has already loaded (and the animation has
|
||||||
|
// already finished), falling back to applying it ourselves:
|
||||||
|
maybeApplyMainStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {}
|
||||||
|
}).exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void maybeApplyContext() {
|
||||||
|
if (!transitionFinished || result == null || getContext() == null) return;
|
||||||
|
Map<String, Status> oldData = null;
|
||||||
|
if(refreshing){
|
||||||
|
oldData = new HashMap<>(data.size());
|
||||||
|
for (Status s : data) oldData.put(s.id, s);
|
||||||
|
data.clear();
|
||||||
|
ancestryMap.clear();
|
||||||
|
displayItems.clear();
|
||||||
|
data.add(mainStatus);
|
||||||
|
onAppendItems(Collections.singletonList(mainStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: figure out how this code works
|
||||||
|
if (isInstanceAkkoma()) sortStatusContext(mainStatus, result);
|
||||||
|
|
||||||
|
result.descendants=filterStatuses(result.descendants);
|
||||||
|
result.ancestors=filterStatuses(result.ancestors);
|
||||||
|
|
||||||
|
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
|
||||||
|
ancestryMap.put(i.status.id, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(footerProgress!=null)
|
||||||
|
footerProgress.setVisibility(View.GONE);
|
||||||
|
data.addAll(result.descendants);
|
||||||
|
int prevCount=displayItems.size();
|
||||||
|
onAppendItems(result.descendants);
|
||||||
|
int count=displayItems.size();
|
||||||
|
if(!refreshing)
|
||||||
|
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
|
||||||
|
int prependedCount = prependItems(result.ancestors, !refreshing);
|
||||||
|
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
|
||||||
|
displayItems.remove(prependedCount);
|
||||||
|
adapter.notifyItemRemoved(prependedCount);
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Status s : data) {
|
||||||
|
Status oldStatus = oldData == null ? null : oldData.get(s.id);
|
||||||
|
// restore previous spoiler/filter revealed states when refreshing
|
||||||
|
if (oldStatus != null) {
|
||||||
|
s.spoilerRevealed = oldStatus.spoilerRevealed;
|
||||||
|
s.filterRevealed = oldStatus.filterRevealed;
|
||||||
|
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
|
||||||
|
s.spoilerText != null &&
|
||||||
|
s.spoilerText.equals(mainStatus.spoilerText) &&
|
||||||
|
mainStatus.spoilerRevealed) {
|
||||||
|
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
|
||||||
|
s.spoilerRevealed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataLoaded();
|
||||||
|
if(refreshing){
|
||||||
|
refreshDone();
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
list.scrollToPosition(displayItems.size()-count);
|
||||||
|
|
||||||
|
// no animation is going to happen, so proceeding to apply right now
|
||||||
|
if (data.size() == 1) {
|
||||||
|
contextInitiallyRendered = true;
|
||||||
|
// for the case that the main status has already finished loading
|
||||||
|
maybeApplyMainStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Object maybeApplyMainStatus() {
|
||||||
|
if (updatedStatus == null || !contextInitiallyRendered) return null;
|
||||||
|
|
||||||
|
// restore revealed states for main status because it gets updated after doLoadData
|
||||||
|
updatedStatus.filterRevealed = mainStatus.filterRevealed;
|
||||||
|
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
|
||||||
|
|
||||||
|
// returning fired event object to facilitate testing
|
||||||
|
Object event;
|
||||||
|
if (updatedStatus.editedAt != null &&
|
||||||
|
(mainStatus.editedAt == null ||
|
||||||
|
updatedStatus.editedAt.isAfter(mainStatus.editedAt))) {
|
||||||
|
event = new StatusUpdatedEvent(updatedStatus);
|
||||||
|
} else {
|
||||||
|
event = new StatusCountersUpdatedEvent(updatedStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainStatus = updatedStatus;
|
||||||
|
updatedStatus = null;
|
||||||
|
E.post(event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
|
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
|
||||||
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
|
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
|
||||||
|
|
||||||
@@ -178,22 +255,21 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
int count = statuses.size();
|
int count = statuses.size();
|
||||||
for (int index = 0; index < count; index++) {
|
for (int index = 0; index < count; index++) {
|
||||||
Status current = statuses.get(index);
|
Status current = statuses.get(index);
|
||||||
NeighborAncestryInfo item = new NeighborAncestryInfo(current);
|
ancestry.add(new NeighborAncestryInfo(
|
||||||
|
current,
|
||||||
item.descendantNeighbor = Optional
|
// descendant neighbor
|
||||||
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
|
Optional
|
||||||
.filter(s -> s.inReplyToId.equals(current.id))
|
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
|
||||||
.orElse(null);
|
.filter(s -> s.inReplyToId.equals(current.id))
|
||||||
|
.orElse(null),
|
||||||
item.ancestoringNeighbor = Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
|
// ancestoring neighbor
|
||||||
.filter(ancestor -> ancestor
|
Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
|
||||||
.getDescendantNeighbor()
|
.filter(ancestor -> Optional.ofNullable(ancestor.descendantNeighbor)
|
||||||
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
|
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
|
||||||
.orElse(false))
|
.orElse(false))
|
||||||
.flatMap(NeighborAncestryInfo::getStatus)
|
.map(a -> a.status)
|
||||||
.orElse(null);
|
.orElse(null)
|
||||||
|
));
|
||||||
ancestry.add(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ancestry;
|
return ancestry;
|
||||||
@@ -263,17 +339,78 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
showContent();
|
showContent();
|
||||||
if(!loaded)
|
if(!loaded)
|
||||||
footerProgress.setVisibility(View.VISIBLE);
|
footerProgress.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
list.setItemAnimator(new BetterItemAnimator() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
super.onAnimationFinished(viewHolder);
|
||||||
|
contextInitiallyRendered = true;
|
||||||
|
// for the case that both requests are already done (and thus won't apply it)
|
||||||
|
maybeApplyMainStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||||
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
|
if (ev.status.inReplyToId == null) return;
|
||||||
onAppendItems(Collections.singletonList(ev.status));
|
Status repliedToStatus = getStatusByID(ev.status.inReplyToId);
|
||||||
|
if (repliedToStatus == null) return;
|
||||||
|
NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id);
|
||||||
|
|
||||||
|
int nextDisplayItemsIndex = -1, indexOfPreviousDisplayItem = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < displayItems.size(); i++) {
|
||||||
|
StatusDisplayItem item = displayItems.get(i);
|
||||||
|
if (repliedToStatus.id.equals(item.parentID)) {
|
||||||
|
// saving the replied-to status' display items index to eventually reach the last one
|
||||||
|
indexOfPreviousDisplayItem = i;
|
||||||
|
item.hasDescendantNeighbor = true;
|
||||||
|
} else if (indexOfPreviousDisplayItem >= 0 && nextDisplayItemsIndex == -1) {
|
||||||
|
// previous display item was the replied-to status' display items
|
||||||
|
nextDisplayItemsIndex = i;
|
||||||
|
// nothing left to do if there's no other reply to that status
|
||||||
|
if (ancestry.descendantNeighbor == null) break;
|
||||||
|
}
|
||||||
|
if (ancestry.descendantNeighbor != null && item.parentID.equals(ancestry.descendantNeighbor.id)) {
|
||||||
|
// existing reply shall no longer have the replied-to status as its neighbor
|
||||||
|
item.hasAncestoringNeighbor = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fall back to inserting the item at the end
|
||||||
|
nextDisplayItemsIndex = nextDisplayItemsIndex >= 0 ? nextDisplayItemsIndex : displayItems.size();
|
||||||
|
int nextDataIndex = data.indexOf(repliedToStatus) + 1;
|
||||||
|
|
||||||
|
// if replied-to status already has another reply...
|
||||||
|
if (ancestry.descendantNeighbor != null) {
|
||||||
|
// update the reply's ancestry to remove its ancestoring neighbor (as we did above)
|
||||||
|
ancestryMap.get(ancestry.descendantNeighbor.id).ancestoringNeighbor = null;
|
||||||
|
// make sure the existing reply has a reply line
|
||||||
|
if (nextDataIndex < data.size() &&
|
||||||
|
!(displayItems.get(nextDisplayItemsIndex) instanceof ReblogOrReplyLineStatusDisplayItem)) {
|
||||||
|
Status nextStatus = data.get(nextDataIndex);
|
||||||
|
if (!nextStatus.account.id.equals(repliedToStatus.account.id)) {
|
||||||
|
// create reply line manually since we're not building that status' items
|
||||||
|
displayItems.add(nextDisplayItemsIndex, StatusDisplayItem.buildReplyLine(
|
||||||
|
this, nextStatus, accountID, nextStatus, repliedToStatus.account, false
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update replied-to status' ancestry
|
||||||
|
ancestry.descendantNeighbor = ev.status;
|
||||||
|
|
||||||
|
// add ancestry for newly created status before building its display items
|
||||||
|
ancestryMap.put(ev.status.id, new NeighborAncestryInfo(ev.status, null, repliedToStatus));
|
||||||
|
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(ev.status));
|
||||||
|
data.add(nextDataIndex, ev.status);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isItemEnabled(String id){
|
public boolean isItemEnabled(String id){
|
||||||
return !id.equals(mainStatus.id);
|
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -297,31 +434,13 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
return Uri.parse(mainStatus.url);
|
return Uri.parse(mainStatus.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class NeighborAncestryInfo {
|
protected static class NeighborAncestryInfo {
|
||||||
protected Status status, descendantNeighbor, ancestoringNeighbor;
|
protected Status status, descendantNeighbor, ancestoringNeighbor;
|
||||||
|
|
||||||
public NeighborAncestryInfo(@NonNull Status status) {
|
protected NeighborAncestryInfo(@NonNull Status status, Status descendantNeighbor, Status ancestoringNeighbor) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
this.descendantNeighbor = descendantNeighbor;
|
||||||
|
this.ancestoringNeighbor = ancestoringNeighbor;
|
||||||
public Optional<Status> getStatus() {
|
|
||||||
return Optional.ofNullable(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Status> getDescendantNeighbor() {
|
|
||||||
return Optional.ofNullable(descendantNeighbor);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<Status> getAncestoringNeighbor() {
|
|
||||||
return Optional.ofNullable(ancestoringNeighbor);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasDescendantNeighbor() {
|
|
||||||
return getDescendantNeighbor().isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasAncestoringNeighbor() {
|
|
||||||
return getAncestoringNeighbor().isPresent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -339,4 +458,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
|||||||
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
|
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onErrorRetryClick(){
|
||||||
|
if(preloadingFailed){
|
||||||
|
preloadingFailed=false;
|
||||||
|
V.setVisibilityAnimated(footerProgress, View.VISIBLE);
|
||||||
|
V.setVisibilityAnimated(footerError, View.GONE);
|
||||||
|
doLoadData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.onErrorRetryClick();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.requests.accounts.GetAccountByHandle;
|
||||||
|
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.Account;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment{
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class AccountRelatedAccountListFragment extends PaginatedAccountListFragment<Account> {
|
||||||
protected Account account;
|
protected Account account;
|
||||||
|
protected String initialSubtitle = "";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||||
|
if (getArguments().containsKey("remoteAccount")) {
|
||||||
|
remoteInfo = Parcels.unwrap(getArguments().getParcelable("remoteAccount"));
|
||||||
|
}
|
||||||
setTitle("@"+account.acct);
|
setTitle("@"+account.acct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,4 +33,36 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
|
|||||||
? "/users/" + account.id
|
? "/users/" + account.id
|
||||||
: '@' + account.acct).build();
|
: '@' + account.acct).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteDomain() {
|
||||||
|
return account.getDomainFromURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Account getCurrentInfo() {
|
||||||
|
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : account;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MastodonAPIRequest<Account> loadRemoteInfo() {
|
||||||
|
return new GetAccountByHandle(account.acct);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AccountSession getRemoteSession() {
|
||||||
|
return Optional.ofNullable(remoteInfo)
|
||||||
|
.map(AccountSessionManager.getInstance()::tryGetAccount)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRemoteLoadingFailed() {
|
||||||
|
super.onRemoteLoadingFailed();
|
||||||
|
String prefix = initialSubtitle == null ? "" :
|
||||||
|
initialSubtitle + " " + getContext().getString(R.string.sk_separator) + " ";
|
||||||
|
String str = prefix +
|
||||||
|
getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain);
|
||||||
|
setSubtitle(str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.fragments.account_list;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.app.assist.AssistContent;
|
import android.app.assist.AssistContent;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -47,6 +48,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.APIRequest;
|
import me.grishka.appkit.api.APIRequest;
|
||||||
@@ -243,16 +245,19 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
|
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
@Override
|
@Override
|
||||||
public void onBind(AccountItem item){
|
public void onBind(AccountItem item){
|
||||||
name.setText(item.parsedName);
|
name.setText(item.parsedName);
|
||||||
username.setText("@"+item.account.acct);
|
username.setText("@"+ (item.account.isRemote
|
||||||
|
? item.account.getFullyQualifiedName()
|
||||||
|
: item.account.acct));
|
||||||
bindRelationship();
|
bindRelationship();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void bindRelationship(){
|
public void bindRelationship(){
|
||||||
Relationship rel=relationships.get(item.account.id);
|
Relationship rel=relationships.get(item.account.id);
|
||||||
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||||
button.setVisibility(View.GONE);
|
button.setVisibility(View.GONE);
|
||||||
}else{
|
}else{
|
||||||
button.setVisibility(View.VISIBLE);
|
button.setVisibility(View.VISIBLE);
|
||||||
@@ -282,7 +287,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
public void onClick(){
|
public void onClick(){
|
||||||
Bundle args=new Bundle();
|
Bundle args=new Bundle();
|
||||||
args.putString("account", accountID);
|
args.putString("account", accountID);
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
|
||||||
|
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,5 +429,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
|
|||||||
emojiHelper=new CustomEmojiHelper();
|
emojiHelper=new CustomEmojiHelper();
|
||||||
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
return obj instanceof AccountItem i && i.account.url.equals(account.url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
|
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||||
return new GetAccountFollowers(account.id, maxID, count);
|
return new GetAccountFollowers(getCurrentInfo().id, maxID, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
|
setSubtitle(initialSubtitle = getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||||
return new GetAccountFollowing(account.id, maxID, count);
|
return new GetAccountFollowing(getCurrentInfo().id, maxID, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,33 +1,173 @@
|
|||||||
package org.joinmastodon.android.fragments.account_list;
|
package org.joinmastodon.android.fragments.account_list;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
public abstract class PaginatedAccountListFragment extends BaseAccountListFragment{
|
public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFragment{
|
||||||
private String nextMaxID;
|
private String nextMaxID;
|
||||||
|
private MastodonAPIRequest<T> remoteInfoRequest;
|
||||||
|
protected boolean doneWithHomeInstance, remoteRequestFailed, startedRemoteLoading, remoteDisabled;
|
||||||
|
protected int localOffset;
|
||||||
|
protected T remoteInfo;
|
||||||
|
|
||||||
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
|
public abstract HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count);
|
||||||
|
|
||||||
|
protected abstract MastodonAPIRequest<T> loadRemoteInfo();
|
||||||
|
public abstract T getCurrentInfo();
|
||||||
|
public abstract String getRemoteDomain();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
// already have remote info (e.g. from arguments), so no need to fetch it again
|
||||||
|
if (remoteInfo != null) {
|
||||||
|
onRemoteInfoLoaded(remoteInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteDisabled = !GlobalUserPreferences.allowRemoteLoading
|
||||||
|
|| getSession().domain.equals(getRemoteDomain());
|
||||||
|
if (!remoteDisabled) {
|
||||||
|
remoteInfoRequest = loadRemoteInfo().setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(T result) {
|
||||||
|
if (getContext() == null) return;
|
||||||
|
onRemoteInfoLoaded(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
if (getContext() == null) return;
|
||||||
|
onRemoteLoadingFailed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
remoteInfoRequest.execRemote(getRemoteDomain(), getRemoteSession());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* override to provide an ideal account session (e.g. if you're logged into the author's remote
|
||||||
|
* account) to make the remote request from. if null is provided, will try to get any session
|
||||||
|
* on the remote domain, or tries the request without authentication.
|
||||||
|
*/
|
||||||
|
protected AccountSession getRemoteSession() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onRemoteInfoLoaded(T info) {
|
||||||
|
this.remoteInfo = info;
|
||||||
|
this.remoteInfoRequest = null;
|
||||||
|
maybeStartLoadingRemote();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onRemoteLoadingFailed() {
|
||||||
|
this.remoteRequestFailed = true;
|
||||||
|
this.remoteInfo = null;
|
||||||
|
this.remoteInfoRequest = null;
|
||||||
|
if (doneWithHomeInstance) dataLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dataLoaded() {
|
||||||
|
super.dataLoaded();
|
||||||
|
footerProgress.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeStartLoadingRemote() {
|
||||||
|
if (startedRemoteLoading || remoteDisabled) return;
|
||||||
|
if (!remoteRequestFailed) {
|
||||||
|
if (data.size() == 0) showProgress();
|
||||||
|
else footerProgress.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
if (doneWithHomeInstance && remoteInfo != null) {
|
||||||
|
startedRemoteLoading = true;
|
||||||
|
loadData(localOffset, itemsPerPage * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRefresh() {
|
||||||
|
localOffset = 0;
|
||||||
|
doneWithHomeInstance = false;
|
||||||
|
startedRemoteLoading = false;
|
||||||
|
super.onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadData(int offset, int count) {
|
||||||
|
// always subtract the amount loaded through the home instance once loading from remote
|
||||||
|
// since loadData gets called with data.size() (data includes both local and remote)
|
||||||
|
if (doneWithHomeInstance) offset -= localOffset;
|
||||||
|
super.loadData(offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
currentRequest=onCreateRequest(offset==0 ? null : nextMaxID, count)
|
MastodonAPIRequest<?> request = onCreateRequest(offset==0 ? null : nextMaxID, count)
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(HeaderPaginationList<Account> result){
|
public void onSuccess(HeaderPaginationList<Account> result){
|
||||||
|
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
|
||||||
|
Collection<AccountItem> d = justRefreshed ? List.of() : data;
|
||||||
|
|
||||||
if(result.nextPageUri!=null)
|
if(result.nextPageUri!=null)
|
||||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||||
else
|
else
|
||||||
nextMaxID=null;
|
nextMaxID=null;
|
||||||
if (getActivity() == null) return;
|
if (getActivity() == null) return;
|
||||||
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
|
List<AccountItem> items = result.stream()
|
||||||
|
.filter(a -> d.size() > 1000 || d.stream()
|
||||||
|
.noneMatch(i -> i.account.url.equals(a.url)))
|
||||||
|
.map(AccountItem::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
boolean hasMore = nextMaxID != null;
|
||||||
|
|
||||||
|
if (!hasMore && !doneWithHomeInstance) {
|
||||||
|
// only runs last time data was fetched from the home instance
|
||||||
|
localOffset = d.size() + items.size();
|
||||||
|
doneWithHomeInstance = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataLoaded(items, hasMore);
|
||||||
|
if (doneWithHomeInstance) maybeStartLoadingRemote();
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.exec(accountID);
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
if (doneWithHomeInstance) {
|
||||||
|
onRemoteLoadingFailed();
|
||||||
|
onDataLoaded(Collections.emptyList(), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.onError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (doneWithHomeInstance && remoteInfo == null) return; // we are waiting
|
||||||
|
if (doneWithHomeInstance && remoteInfo != null) {
|
||||||
|
request.execRemote(getRemoteDomain(), getRemoteSession());
|
||||||
|
} else {
|
||||||
|
request.exec(accountID);
|
||||||
|
}
|
||||||
|
currentRequest = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
|
import org.joinmastodon.android.api.requests.statuses.GetStatusFavorites;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
|
public class StatusFavoritesListFragment extends StatusRelatedAccountListFragment{
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
updateTitle(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateTitle(Status status) {
|
||||||
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
|
setTitle(getResources().getQuantityString(R.plurals.x_favorites, (int)(status.favouritesCount%1000), status.favouritesCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||||
return new GetStatusFavorites(status.id, maxID, count);
|
return new GetStatusFavorites(getCurrentInfo().id, maxID, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,17 +7,23 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
|
import org.joinmastodon.android.api.requests.statuses.GetStatusReblogs;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
|
||||||
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
|
public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
updateTitle(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateTitle(Status status) {
|
||||||
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
|
setTitle(getResources().getQuantityString(R.plurals.x_reblogs, (int)(status.reblogsCount%1000), status.reblogsCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||||
return new GetStatusReblogs(status.id, maxID, count);
|
return new GetStatusReblogs(getCurrentInfo().id, maxID, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,12 +3,27 @@ package org.joinmastodon.android.fragments.account_list;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSession;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment{
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public abstract class StatusRelatedAccountListFragment extends PaginatedAccountListFragment<Status> {
|
||||||
protected Status status;
|
protected Status status;
|
||||||
|
|
||||||
|
protected abstract void updateTitle(Status status);
|
||||||
|
|
||||||
|
protected MastodonAPIRequest<Status> loadRemoteInfo() {
|
||||||
|
String[] parts = status.url.split("/");
|
||||||
|
if (parts.length == 0) return null;
|
||||||
|
return new GetStatusByID(parts[parts.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -17,7 +32,7 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean hasSubtitle(){
|
protected boolean hasSubtitle(){
|
||||||
return false;
|
return remoteRequestFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -28,4 +43,35 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
|
|||||||
: '@' + status.account.acct + '/' + status.id)
|
: '@' + status.account.acct + '/' + status.id)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteDomain() {
|
||||||
|
return Uri.parse(status.url).getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Status getCurrentInfo() {
|
||||||
|
return doneWithHomeInstance && remoteInfo != null ? remoteInfo : status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AccountSession getRemoteSession() {
|
||||||
|
return Optional.ofNullable(remoteInfo)
|
||||||
|
.map(s -> s.account)
|
||||||
|
.map(AccountSessionManager.getInstance()::tryGetAccount)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRemoteInfoLoaded(Status info) {
|
||||||
|
super.onRemoteInfoLoaded(info);
|
||||||
|
updateTitle(remoteInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onRemoteLoadingFailed() {
|
||||||
|
super.onRemoteLoadingFailed();
|
||||||
|
setSubtitle(getContext().getString(R.string.sk_no_remote_info_hint, getSession().domain));
|
||||||
|
updateToolbar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,10 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
|
|||||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||||
|
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
relationship=relationships.get(item.account.id);
|
relationship=relationships.get(item.account.id);
|
||||||
if(relationship==null){
|
if(relationship==null){
|
||||||
actionWrap.setVisibility(View.GONE);
|
actionWrap.setVisibility(View.GONE);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.os.Bundle;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
||||||
import org.joinmastodon.android.fragments.IsOnTop;
|
|
||||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
@@ -17,7 +16,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
|
||||||
public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop {
|
public class DiscoverPostsFragment extends StatusListFragment {
|
||||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
|
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -39,11 +38,6 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
|
|||||||
bannerHelper.maybeAddBanner(contentWrap);
|
bannerHelper.maybeAddBanner(contentWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOnTop() {
|
|
||||||
return isRecyclerViewOnTop(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Filter.FilterContext getFilterContext() {
|
protected Filter.FilterContext getFilterContext() {
|
||||||
return Filter.FilterContext.PUBLIC;
|
return Filter.FilterContext.PUBLIC;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import org.joinmastodon.android.R;
|
|||||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.fragments.IsOnTop;
|
|
||||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
import org.joinmastodon.android.model.Account;
|
||||||
@@ -43,7 +42,7 @@ import me.grishka.appkit.api.ErrorResponse;
|
|||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
|
public class SearchFragment extends BaseStatusListFragment<SearchResult> {
|
||||||
private String currentQuery;
|
private String currentQuery;
|
||||||
private List<StatusDisplayItem> prevDisplayItems;
|
private List<StatusDisplayItem> prevDisplayItems;
|
||||||
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
|
||||||
@@ -312,11 +311,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOnTop() {
|
|
||||||
return isRecyclerViewOnTop(list);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Uri getWebUri(Uri.Builder base) {
|
public Uri getWebUri(Uri.Builder base) {
|
||||||
Uri.Builder searchUri = base.path("/search");
|
Uri.Builder searchUri = base.path("/search");
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import android.widget.TextView;
|
|||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.reports.SendReport;
|
import org.joinmastodon.android.api.requests.reports.SendReport;
|
||||||
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
|
||||||
@@ -39,7 +40,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
|||||||
private TextView forwardReportText;
|
private TextView forwardReportText;
|
||||||
private Switch forwardReportSwitch;
|
private Switch forwardReportSwitch;
|
||||||
private EditText commentEdit;
|
private EditText commentEdit;
|
||||||
private boolean forwardReport;
|
private boolean forwardReport = GlobalUserPreferences.forwardReportDefault;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
@@ -89,7 +90,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
|
|||||||
} else {
|
} else {
|
||||||
forwardReportItem.setOnClickListener(this::onForwardReportClick);
|
forwardReportItem.setOnClickListener(this::onForwardReportClick);
|
||||||
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
|
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
|
||||||
forwardReportSwitch.setChecked(forwardReport = true);
|
forwardReportSwitch.setChecked(forwardReport);
|
||||||
}
|
}
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package org.joinmastodon.android.model;
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
@@ -62,7 +65,6 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
/**
|
/**
|
||||||
* An image banner that is shown above the profile and in profile cards.
|
* An image banner that is shown above the profile and in profile cards.
|
||||||
*/
|
*/
|
||||||
@RequiredField
|
|
||||||
public String header;
|
public String header;
|
||||||
/**
|
/**
|
||||||
* A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.
|
* A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.
|
||||||
@@ -135,6 +137,8 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
|
|
||||||
public List<Role> roles;
|
public List<Role> roles;
|
||||||
|
|
||||||
|
public @Nullable String fqn; // akkoma has this, mastodon't
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getQuery() {
|
public String getQuery() {
|
||||||
return url;
|
return url;
|
||||||
@@ -162,6 +166,7 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
moved.postprocess();
|
moved.postprocess();
|
||||||
if(TextUtils.isEmpty(displayName))
|
if(TextUtils.isEmpty(displayName))
|
||||||
displayName=username;
|
displayName=username;
|
||||||
|
if(fqn == null) fqn = getFullyQualifiedName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLocal(){
|
public boolean isLocal(){
|
||||||
@@ -173,6 +178,10 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
return parts.length==1 ? null : parts[1];
|
return parts.length==1 ? null : parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDomainFromURL() {
|
||||||
|
return Uri.parse(url).getHost();
|
||||||
|
}
|
||||||
|
|
||||||
public String getDisplayUsername(){
|
public String getDisplayUsername(){
|
||||||
return '@'+acct;
|
return '@'+acct;
|
||||||
}
|
}
|
||||||
@@ -181,6 +190,10 @@ public class Account extends BaseModel implements Searchable{
|
|||||||
return '@'+acct.split("@")[0];
|
return '@'+acct.split("@")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFullyQualifiedName() {
|
||||||
|
return fqn != null ? fqn : acct.split("@")[0] + "@" + getDomainFromURL();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(){
|
public String toString(){
|
||||||
return "Account{"+
|
return "Account{"+
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ import java.lang.reflect.Field;
|
|||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public abstract class BaseModel implements Cloneable{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* indicates the profile has been fetched from a foreign instance.
|
||||||
|
*
|
||||||
|
* @see MastodonAPIRequest#execRemote
|
||||||
|
*/
|
||||||
|
public transient boolean isRemote;
|
||||||
|
|
||||||
public abstract class BaseModel{
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
public void postprocess() throws ObjectValidationException{
|
public void postprocess() throws ObjectValidationException{
|
||||||
try{
|
try{
|
||||||
@@ -23,4 +32,14 @@ public abstract class BaseModel{
|
|||||||
}
|
}
|
||||||
}catch(IllegalAccessException ignore){}
|
}catch(IllegalAccessException ignore){}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Object clone(){
|
||||||
|
try{
|
||||||
|
return super.clone();
|
||||||
|
}catch(CloneNotSupportedException x){
|
||||||
|
throw new RuntimeException(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ public class Instance extends BaseModel{
|
|||||||
return pleroma != null;
|
return pleroma != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPixelfed() {
|
||||||
|
return version.contains("compatible; Pixelfed");
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasFeature(Feature feature) {
|
public boolean hasFeature(Feature feature) {
|
||||||
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
|
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
|
||||||
.map(p -> p.metadata)
|
.map(p -> p.metadata)
|
||||||
|
|||||||
@@ -30,10 +30,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
|||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public PushSubscription clone(){
|
public PushSubscription clone(){
|
||||||
PushSubscription copy=null;
|
PushSubscription copy=(PushSubscription) super.clone();
|
||||||
try{
|
|
||||||
copy=(PushSubscription) super.clone();
|
|
||||||
}catch(CloneNotSupportedException ignore){}
|
|
||||||
copy.alerts=alerts.clone();
|
copy.alerts=alerts.clone();
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.joinmastodon.android.model;
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
import org.joinmastodon.android.model.Poll.Option;
|
import org.joinmastodon.android.model.Poll.Option;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
@@ -16,7 +17,6 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
|||||||
public Instant scheduledAt;
|
public Instant scheduledAt;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public Params params;
|
public Params params;
|
||||||
@RequiredField
|
|
||||||
public List<Attachment> mediaAttachments;
|
public List<Attachment> mediaAttachments;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -24,8 +24,17 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess() throws ObjectValidationException {
|
||||||
|
super.postprocess();
|
||||||
|
if (mediaAttachments == null) mediaAttachments = List.of();
|
||||||
|
for(Attachment a:mediaAttachments)
|
||||||
|
a.postprocess();
|
||||||
|
if (params != null) params.postprocess();
|
||||||
|
}
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public static class Params {
|
public static class Params extends BaseModel {
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String text;
|
public String text;
|
||||||
public String spoilerText;
|
public String spoilerText;
|
||||||
@@ -40,10 +49,16 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
|||||||
public String applicationId;
|
public String applicationId;
|
||||||
public List<String> mediaIds;
|
public List<String> mediaIds;
|
||||||
public ContentType contentType;
|
public ContentType contentType;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess() throws ObjectValidationException {
|
||||||
|
super.postprocess();
|
||||||
|
if (poll != null) poll.postprocess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcel
|
@Parcel
|
||||||
public static class ScheduledPoll {
|
public static class ScheduledPoll extends BaseModel {
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String expiresIn;
|
public String expiresIn;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.google.gson.JsonParseException;
|
|||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
import org.joinmastodon.android.api.ObjectValidationException;
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
import org.joinmastodon.android.api.RequiredField;
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.parceler.Parcel;
|
import org.parceler.Parcel;
|
||||||
@@ -37,7 +38,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
public boolean sensitive;
|
public boolean sensitive;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
public String spoilerText;
|
public String spoilerText;
|
||||||
@RequiredField
|
|
||||||
public List<Attachment> mediaAttachments;
|
public List<Attachment> mediaAttachments;
|
||||||
public Application application;
|
public Application application;
|
||||||
@RequiredField
|
@RequiredField
|
||||||
@@ -94,6 +94,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
t.postprocess();
|
t.postprocess();
|
||||||
for(Emoji e:emojis)
|
for(Emoji e:emojis)
|
||||||
e.postprocess();
|
e.postprocess();
|
||||||
|
if (mediaAttachments == null) mediaAttachments = List.of();
|
||||||
for(Attachment a:mediaAttachments)
|
for(Attachment a:mediaAttachments)
|
||||||
a.postprocess();
|
a.postprocess();
|
||||||
account.postprocess();
|
account.postprocess();
|
||||||
@@ -171,6 +172,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
return strippedText;
|
return strippedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReblogPermitted(String accountID){
|
||||||
|
return visibility.isReblogPermitted(account.id.equals(
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).self.id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
public static Status ofFake(String id, String text, Instant createdAt) {
|
public static Status ofFake(String id, String text, Instant createdAt) {
|
||||||
Status s = new Status();
|
Status s = new Status();
|
||||||
s.id = id;
|
s.id = id;
|
||||||
@@ -191,7 +198,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class StatusDeserializer implements JsonDeserializer<Status> {
|
public static class StatusDeserializer implements JsonDeserializer<Status> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
public Status deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||||
JsonObject obj = json.getAsJsonObject();
|
JsonObject obj = json.getAsJsonObject();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public enum StatusPrivacy{
|
|||||||
@SerializedName("local")
|
@SerializedName("local")
|
||||||
LOCAL(4); // akkoma
|
LOCAL(4); // akkoma
|
||||||
|
|
||||||
private int privacy;
|
private final int privacy;
|
||||||
|
|
||||||
StatusPrivacy(int privacy) {
|
StatusPrivacy(int privacy) {
|
||||||
this.privacy = privacy;
|
this.privacy = privacy;
|
||||||
@@ -24,6 +24,13 @@ public enum StatusPrivacy{
|
|||||||
return privacy > other.getPrivacy();
|
return privacy > other.getPrivacy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isReblogPermitted(boolean isOwnStatus){
|
||||||
|
return (this == StatusPrivacy.PUBLIC ||
|
||||||
|
this == StatusPrivacy.UNLISTED ||
|
||||||
|
this == StatusPrivacy.LOCAL ||
|
||||||
|
(this == StatusPrivacy.PRIVATE && isOwnStatus));
|
||||||
|
}
|
||||||
|
|
||||||
public int getPrivacy() {
|
public int getPrivacy() {
|
||||||
return privacy;
|
return privacy;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,24 +58,23 @@ import me.grishka.appkit.views.UsableRecyclerView;
|
|||||||
public class AccountSwitcherSheet extends BottomSheet{
|
public class AccountSwitcherSheet extends BottomSheet{
|
||||||
private final Activity activity;
|
private final Activity activity;
|
||||||
private final HomeFragment fragment;
|
private final HomeFragment fragment;
|
||||||
private final BiConsumer<String, Boolean> onClick;
|
|
||||||
private final boolean externalShare, openInApp;
|
private final boolean externalShare, openInApp;
|
||||||
|
private BiConsumer<String, Boolean> onClick;
|
||||||
private UsableRecyclerView list;
|
private UsableRecyclerView list;
|
||||||
private List<WrappedAccount> accounts;
|
private List<WrappedAccount> accounts;
|
||||||
private ListImageLoaderWrapper imgLoader;
|
private ListImageLoaderWrapper imgLoader;
|
||||||
private AccountsAdapter accountsAdapter;
|
private AccountsAdapter accountsAdapter;
|
||||||
|
|
||||||
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
|
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
|
||||||
this(activity, fragment, false, false, null);
|
this(activity, fragment, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp, BiConsumer<String, Boolean> onClick){
|
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp){
|
||||||
super(activity);
|
super(activity);
|
||||||
this.activity=activity;
|
this.activity=activity;
|
||||||
this.fragment=fragment;
|
this.fragment=fragment;
|
||||||
this.externalShare = externalShare;
|
this.externalShare = externalShare;
|
||||||
this.openInApp = openInApp;
|
this.openInApp = openInApp;
|
||||||
this.onClick = onClick;
|
|
||||||
|
|
||||||
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
|
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||||
|
|
||||||
@@ -123,6 +122,10 @@ public class AccountSwitcherSheet extends BottomSheet{
|
|||||||
UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnClick(BiConsumer<String, Boolean> onClick) {
|
||||||
|
this.onClick = onClick;
|
||||||
|
}
|
||||||
|
|
||||||
private void confirmLogOut(String accountID){
|
private void confirmLogOut(String accountID){
|
||||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
new M3AlertDialogBuilder(activity)
|
new M3AlertDialogBuilder(activity)
|
||||||
|
|||||||
@@ -10,153 +10,170 @@ import java.util.List;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class PhotoLayoutHelper{
|
public class PhotoLayoutHelper{
|
||||||
public static final int MAX_WIDTH=1000;
|
public static final int MAX_WIDTH=1000;
|
||||||
public static final int MAX_HEIGHT=1910;
|
public static final int MAX_HEIGHT=1700;
|
||||||
|
public static final float GAP=1.5f;
|
||||||
|
|
||||||
|
// 2 * margin + close button height - gap. i don't know if the gap subtraction is correct
|
||||||
|
public static final int MIN_HEIGHT = Math.round(V.dp(2 * 12) + V.dp(40) - GAP);
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static TiledLayoutResult processThumbs(List<Attachment> thumbs){
|
public static TiledLayoutResult processThumbs(List<Attachment> thumbs){
|
||||||
int _maxW=MAX_WIDTH;
|
float maxRatio=MAX_WIDTH/(float)MAX_HEIGHT;
|
||||||
int _maxH=MAX_HEIGHT;
|
|
||||||
|
|
||||||
TiledLayoutResult result=new TiledLayoutResult();
|
TiledLayoutResult result=new TiledLayoutResult();
|
||||||
if(thumbs.size()==1){
|
if(thumbs.size()==1){
|
||||||
Attachment att=thumbs.get(0);
|
Attachment att=thumbs.get(0);
|
||||||
result.rowSizes=result.columnSizes=new int[]{1};
|
result.rowSizes=result.columnSizes=new int[]{1};
|
||||||
if(att.getWidth()>att.getHeight()){
|
float ratio=att.getWidth()/(float) att.getHeight();
|
||||||
result.width=_maxW;
|
if(ratio>maxRatio){
|
||||||
result.height=Math.round(att.getHeight()/(float)att.getWidth()*_maxW);
|
result.width=MAX_WIDTH;
|
||||||
|
result.height=Math.max(MIN_HEIGHT, Math.round(att.getHeight()/(float)att.getWidth()*MAX_WIDTH));
|
||||||
}else{
|
}else{
|
||||||
result.height=_maxH;
|
result.height=MAX_HEIGHT;
|
||||||
result.width=Math.round(att.getWidth()/(float)att.getHeight()*_maxH);
|
result.width=MAX_WIDTH;//Math.round(att.getWidth()/(float)att.getHeight()*MAX_HEIGHT);
|
||||||
}
|
}
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{new TiledLayoutResult.Tile(1, 1, result.width, result.height, 0, 0)};
|
result.tiles=new TiledLayoutResult.Tile[]{new TiledLayoutResult.Tile(1, 1, 0, 0)};
|
||||||
|
return result;
|
||||||
}else if(thumbs.size()==0){
|
}else if(thumbs.size()==0){
|
||||||
throw new IllegalArgumentException("Empty thumbs array");
|
throw new IllegalArgumentException("Empty thumbs array");
|
||||||
}
|
}
|
||||||
|
|
||||||
String orients="";
|
ArrayList<Float> ratios=new ArrayList<>();
|
||||||
ArrayList<Float> ratios=new ArrayList<Float>();
|
|
||||||
int cnt=thumbs.size();
|
int cnt=thumbs.size();
|
||||||
|
|
||||||
|
boolean allAreWide=true, allAreSquare=true;
|
||||||
|
|
||||||
for(Attachment thumb : thumbs){
|
for(Attachment thumb : thumbs){
|
||||||
// float ratio=thumb.isSizeKnown() ? thumb.getWidth()/(float) thumb.getHeight() : 1f;
|
float ratio=Math.max(0.45f, thumb.getWidth()/(float) thumb.getHeight());
|
||||||
float ratio=thumb.getWidth()/(float) thumb.getHeight();
|
if(ratio<=1.2f){
|
||||||
char orient=ratio>1.2 ? 'w' : (ratio<0.8 ? 'n' : 'q');
|
allAreWide=false;
|
||||||
orients+=orient;
|
if(ratio<0.8f)
|
||||||
|
allAreSquare=false;
|
||||||
|
}else{
|
||||||
|
allAreSquare=false;
|
||||||
|
}
|
||||||
ratios.add(ratio);
|
ratios.add(ratio);
|
||||||
}
|
}
|
||||||
|
|
||||||
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
|
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
|
||||||
|
|
||||||
float maxW, maxH, marginW=0, marginH=0;
|
|
||||||
maxW=_maxW;
|
|
||||||
maxH=_maxH;
|
|
||||||
|
|
||||||
float maxRatio=maxW/maxH;
|
|
||||||
|
|
||||||
if(cnt==2){
|
if(cnt==2){
|
||||||
if(orients.equals("ww") && avgRatio>1.4*maxRatio && (ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
|
if(allAreWide && avgRatio>1.4*maxRatio && (ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
|
||||||
float h=Math.min(maxW/ratios.get(0), Math.min(maxW/ratios.get(1), (maxH-marginH)/2.0f));
|
float h=Math.max(Math.min(MAX_WIDTH/ratios.get(0), Math.min(MAX_WIDTH/ratios.get(1), (MAX_HEIGHT-GAP)/2.0f)), MIN_HEIGHT/2f);
|
||||||
|
|
||||||
result.width=Math.round(maxW);
|
result.width=MAX_WIDTH;
|
||||||
result.height=Math.round(h*2+marginH);
|
result.height=Math.round(h*2+GAP);
|
||||||
result.columnSizes=new int[]{result.width};
|
result.columnSizes=new int[]{result.width};
|
||||||
result.rowSizes=new int[]{Math.round(h), Math.round(h)};
|
result.rowSizes=new int[]{Math.round(h), Math.round(h)};
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{
|
result.tiles=new TiledLayoutResult.Tile[]{
|
||||||
new TiledLayoutResult.Tile(1, 1, maxW, h, 0, 0),
|
new TiledLayoutResult.Tile(1, 1, 0, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, maxW, h, 0, 1)
|
new TiledLayoutResult.Tile(1, 1, 0, 1)
|
||||||
};
|
};
|
||||||
}else if(orients.equals("ww") || orients.equals("qq")){ // next to each other, same ratio
|
}else if(allAreWide || allAreSquare){ // next to each other, same ratio
|
||||||
float w=((maxW-marginW)/2);
|
float w=((MAX_WIDTH-GAP)/2);
|
||||||
float h=Math.min(w/ratios.get(0), Math.min(w/ratios.get(1), maxH));
|
float h=Math.max(Math.min(w/ratios.get(0), Math.min(w/ratios.get(1), MAX_HEIGHT)), MIN_HEIGHT);
|
||||||
|
|
||||||
result.width=Math.round(maxW);
|
result.width=MAX_WIDTH;
|
||||||
result.height=Math.round(h);
|
result.height=Math.round(h);
|
||||||
result.columnSizes=new int[]{Math.round(w), _maxW-Math.round(w)};
|
result.columnSizes=new int[]{Math.round(w), MAX_WIDTH-Math.round(w)};
|
||||||
result.rowSizes=new int[]{Math.round(h)};
|
result.rowSizes=new int[]{Math.round(h)};
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{
|
result.tiles=new TiledLayoutResult.Tile[]{
|
||||||
new TiledLayoutResult.Tile(1, 1, w, h, 0, 0),
|
new TiledLayoutResult.Tile(1, 1, 0, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w, h, 1, 0)
|
new TiledLayoutResult.Tile(1, 1, 1, 0)
|
||||||
};
|
};
|
||||||
}else{ // next to each other, different ratios
|
}else{ // next to each other, different ratios
|
||||||
float w0=((maxW-marginW)/ratios.get(1)/(1/ratios.get(0)+1/ratios.get(1)));
|
float w0=((MAX_WIDTH-GAP)/ratios.get(1)/(1/ratios.get(0)+1/ratios.get(1)));
|
||||||
float w1=(maxW-w0-marginW);
|
float w1=(MAX_WIDTH-w0-GAP);
|
||||||
float h=Math.min(maxH, Math.min(w0/ratios.get(0), w1/ratios.get(1)));
|
float h=Math.max(Math.min(MAX_HEIGHT, Math.min(w0/ratios.get(0), w1/ratios.get(1))), MIN_HEIGHT);
|
||||||
|
|
||||||
result.columnSizes=new int[]{Math.round(w0), Math.round(w1)};
|
result.columnSizes=new int[]{Math.round(w0), Math.round(w1)};
|
||||||
result.rowSizes=new int[]{Math.round(h)};
|
result.rowSizes=new int[]{Math.round(h)};
|
||||||
result.width=Math.round(w0+w1+marginW);
|
result.width=Math.round(w0+w1+GAP);
|
||||||
result.height=Math.round(h);
|
result.height=Math.round(h);
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{
|
result.tiles=new TiledLayoutResult.Tile[]{
|
||||||
new TiledLayoutResult.Tile(1, 1, w0, h, 0, 0),
|
new TiledLayoutResult.Tile(1, 1, 0, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w1, h, 1, 0)
|
new TiledLayoutResult.Tile(1, 1, 1, 0)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}else if(cnt==3){
|
}else if(cnt==3){
|
||||||
if(/*(ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) &&*/ orients.equals("www") || true){ // 2nd and 3rd photos are on the next line
|
if((ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) || allAreWide){ // 2nd and 3rd photos are on the next line
|
||||||
float hCover=Math.min(maxW/ratios.get(0), (maxH-marginH)*0.66f);
|
float hCover=Math.min(MAX_WIDTH/ratios.get(0), (MAX_HEIGHT-GAP)*0.66f);
|
||||||
float w2=((maxW-marginW)/2);
|
float w2=((MAX_WIDTH-GAP)/2);
|
||||||
float h=Math.min(maxH-hCover-marginH, Math.min(w2/ratios.get(1), w2/ratios.get(2)));
|
float h=Math.min(MAX_HEIGHT-hCover-GAP, Math.min(w2/ratios.get(1), w2/ratios.get(2)));
|
||||||
result.width=Math.round(maxW);
|
if(hCover+h<MIN_HEIGHT){
|
||||||
result.height=Math.round(hCover+h+marginH);
|
float prevTotalHeight=hCover+h;
|
||||||
result.columnSizes=new int[]{Math.round(w2), _maxW-Math.round(w2)};
|
hCover=MIN_HEIGHT*(hCover/prevTotalHeight);
|
||||||
|
h=MIN_HEIGHT*(h/prevTotalHeight);
|
||||||
|
}
|
||||||
|
result.width=MAX_WIDTH;
|
||||||
|
result.height=Math.round(hCover+h+GAP);
|
||||||
|
result.columnSizes=new int[]{Math.round(w2), MAX_WIDTH-Math.round(w2)};
|
||||||
result.rowSizes=new int[]{Math.round(hCover), Math.round(h)};
|
result.rowSizes=new int[]{Math.round(hCover), Math.round(h)};
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{
|
result.tiles=new TiledLayoutResult.Tile[]{
|
||||||
new TiledLayoutResult.Tile(2, 1, maxW, hCover, 0, 0),
|
new TiledLayoutResult.Tile(2, 1, 0, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w2, h, 0, 1),
|
new TiledLayoutResult.Tile(1, 1, 0, 1),
|
||||||
new TiledLayoutResult.Tile(1, 1, w2, h, 1, 1)
|
new TiledLayoutResult.Tile(1, 1, 1, 1)
|
||||||
};
|
};
|
||||||
}else{ // 2nd and 3rd photos are on the right part
|
}else{ // 2nd and 3rd photos are on the right part
|
||||||
float wCover=Math.min(maxH*ratios.get(0), (maxW-marginW)*0.75f);
|
float height=Math.min(MAX_HEIGHT, MAX_WIDTH*0.66f/avgRatio);
|
||||||
float h1=(ratios.get(1)*(maxH-marginH)/(ratios.get(2)+ratios.get(1)));
|
float wCover=Math.min(height*ratios.get(0), (MAX_WIDTH-GAP)*0.66f);
|
||||||
float h0=(maxH-h1-marginH);
|
float h1=(ratios.get(1)*(height-GAP)/(ratios.get(2)+ratios.get(1)));
|
||||||
float w=Math.min(maxW-wCover-marginW, Math.min(h1*ratios.get(2), h0*ratios.get(1)));
|
float h0=(height-h1-GAP);
|
||||||
result.width=Math.round(wCover+w+marginW);
|
float w=Math.min(MAX_WIDTH-wCover-GAP, Math.min(h1*ratios.get(2), h0*ratios.get(1)));
|
||||||
result.height=Math.round(maxH);
|
result.width=Math.round(wCover+w+GAP);
|
||||||
|
result.height=Math.round(height);
|
||||||
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
|
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
|
||||||
result.rowSizes=new int[]{Math.round(h0), Math.round(h1)};
|
result.rowSizes=new int[]{Math.round(h0), Math.round(h1)};
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{
|
result.tiles=new TiledLayoutResult.Tile[]{
|
||||||
new TiledLayoutResult.Tile(1, 2, wCover, maxH, 0, 0),
|
new TiledLayoutResult.Tile(1, 2, 0, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w, h0, 1, 0),
|
new TiledLayoutResult.Tile(1, 1, 1, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w, h1, 1, 1)
|
new TiledLayoutResult.Tile(1, 1, 1, 1)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}else if(cnt==4){
|
}else if(cnt==4){
|
||||||
if(/*(ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) &&*/ orients.equals("wwww") || true /* temporary fix */){ // 2nd, 3rd and 4th photos are on the next line
|
if((ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) || allAreWide){ // 2nd, 3rd and 4th photos are on the next line
|
||||||
float hCover=Math.min(maxW/ratios.get(0), (maxH-marginH)*0.66f);
|
float hCover=Math.min(MAX_WIDTH/ratios.get(0), (MAX_HEIGHT-GAP)*0.66f);
|
||||||
float h=(maxW-2*marginW)/(ratios.get(1)+ratios.get(2)+ratios.get(3));
|
float h=(MAX_WIDTH-2*GAP)/(ratios.get(1)+ratios.get(2)+ratios.get(3));
|
||||||
float w0=h*ratios.get(1);
|
float w0=h*ratios.get(1);
|
||||||
float w1=h*ratios.get(2);
|
float w1=h*ratios.get(2);
|
||||||
float w2=h*ratios.get(3);
|
float w2=h*ratios.get(3);
|
||||||
h=Math.min(maxH-hCover-marginH, h);
|
h=Math.min(MAX_HEIGHT-hCover-GAP, h);
|
||||||
result.width=Math.round(maxW);
|
if(hCover+h<MIN_HEIGHT){
|
||||||
result.height=Math.round(hCover+h+marginH);
|
float prevTotalHeight=hCover+h;
|
||||||
result.columnSizes=new int[]{Math.round(w0), Math.round(w1), _maxW-Math.round(w0)-Math.round(w1)};
|
hCover=MIN_HEIGHT*(hCover/prevTotalHeight);
|
||||||
|
h=MIN_HEIGHT*(h/prevTotalHeight);
|
||||||
|
}
|
||||||
|
result.width=MAX_WIDTH;
|
||||||
|
result.height=Math.round(hCover+h+GAP);
|
||||||
|
result.columnSizes=new int[]{Math.round(w0), Math.round(w1), MAX_WIDTH-Math.round(w0)-Math.round(w1)};
|
||||||
result.rowSizes=new int[]{Math.round(hCover), Math.round(h)};
|
result.rowSizes=new int[]{Math.round(hCover), Math.round(h)};
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{
|
result.tiles=new TiledLayoutResult.Tile[]{
|
||||||
new TiledLayoutResult.Tile(3, 1, maxW, hCover, 0, 0),
|
new TiledLayoutResult.Tile(3, 1, 0, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w0, h, 0, 1),
|
new TiledLayoutResult.Tile(1, 1, 0, 1),
|
||||||
new TiledLayoutResult.Tile(1, 1, w1, h, 1, 1),
|
new TiledLayoutResult.Tile(1, 1, 1, 1),
|
||||||
new TiledLayoutResult.Tile(1, 1, w2, h, 2, 1),
|
new TiledLayoutResult.Tile(1, 1, 2, 1),
|
||||||
};
|
};
|
||||||
}else{ // 2nd, 3rd and 4th photos are on the right part
|
}else{ // 2nd, 3rd and 4th photos are on the right part
|
||||||
float wCover= Math.min(maxH*ratios.get(0), (maxW-marginW)*0.66f);
|
float height=Math.min(MAX_HEIGHT, MAX_WIDTH*0.66f/avgRatio);
|
||||||
float w=(maxH-2*marginH)/(1/ratios.get(1)+1/ratios.get(2)+1/ratios.get(3));
|
float wCover= Math.min(height*ratios.get(0), (MAX_WIDTH-GAP)*0.66f);
|
||||||
|
float w=(height-2*GAP)/(1/ratios.get(1)+1/ratios.get(2)+1/ratios.get(3));
|
||||||
float h0=w/ratios.get(1);
|
float h0=w/ratios.get(1);
|
||||||
float h1=w/ratios.get(2);
|
float h1=w/ratios.get(2);
|
||||||
float h2=w/ratios.get(3)+marginH;
|
float h2=w/ratios.get(3)+GAP;
|
||||||
w=Math.min(maxW-wCover-marginW, w);
|
w=Math.min(MAX_WIDTH-wCover-GAP, w);
|
||||||
result.width=Math.round(wCover+marginW+w);
|
result.width=Math.round(wCover+GAP+w);
|
||||||
result.height=Math.round(maxH);
|
result.height=Math.round(height);
|
||||||
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
|
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
|
||||||
result.rowSizes=new int[]{Math.round(h0), Math.round(h1), Math.round(h2)};
|
result.rowSizes=new int[]{Math.round(h0), Math.round(h1), Math.round(h2)};
|
||||||
result.tiles=new TiledLayoutResult.Tile[]{
|
result.tiles=new TiledLayoutResult.Tile[]{
|
||||||
new TiledLayoutResult.Tile(1, 3, wCover, maxH, 0, 0),
|
new TiledLayoutResult.Tile(1, 3, 0, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w, h0, 1, 0),
|
new TiledLayoutResult.Tile(1, 1, 1, 0),
|
||||||
new TiledLayoutResult.Tile(1, 1, w, h1, 1, 1),
|
new TiledLayoutResult.Tile(1, 1, 1, 1),
|
||||||
new TiledLayoutResult.Tile(1, 1, w, h2, 1, 2),
|
new TiledLayoutResult.Tile(1, 1, 1, 2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
@@ -174,14 +191,14 @@ public class PhotoLayoutHelper{
|
|||||||
HashMap<int[], float[]> tries=new HashMap<>();
|
HashMap<int[], float[]> tries=new HashMap<>();
|
||||||
|
|
||||||
// One line
|
// One line
|
||||||
int firstLine, secondLine, thirdLine;
|
int firstLine, secondLine;
|
||||||
tries.put(new int[]{firstLine=cnt}, new float[]{calculateMultiThumbsHeight(ratiosCropped, maxW, marginW)});
|
tries.put(new int[]{cnt}, new float[]{calculateMultiThumbsHeight(ratiosCropped, MAX_WIDTH, GAP)});
|
||||||
|
|
||||||
// Two lines
|
// Two lines
|
||||||
for(firstLine=1; firstLine<=cnt-1; firstLine++){
|
for(firstLine=1; firstLine<=cnt-1; firstLine++){
|
||||||
tries.put(new int[]{firstLine, secondLine=cnt-firstLine}, new float[]{
|
tries.put(new int[]{firstLine, cnt-firstLine}, new float[]{
|
||||||
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), maxW, marginW),
|
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), MAX_WIDTH, GAP),
|
||||||
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, ratiosCropped.size()), maxW, marginW)
|
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, ratiosCropped.size()), MAX_WIDTH, GAP)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -189,23 +206,24 @@ public class PhotoLayoutHelper{
|
|||||||
// Three lines
|
// Three lines
|
||||||
for(firstLine=1; firstLine<=cnt-2; firstLine++){
|
for(firstLine=1; firstLine<=cnt-2; firstLine++){
|
||||||
for(secondLine=1; secondLine<=cnt-firstLine-1; secondLine++){
|
for(secondLine=1; secondLine<=cnt-firstLine-1; secondLine++){
|
||||||
tries.put(new int[]{firstLine, secondLine, thirdLine=cnt-firstLine-secondLine}, new float[]{
|
tries.put(new int[]{firstLine, secondLine, cnt-firstLine-secondLine}, new float[]{
|
||||||
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), maxW, marginW),
|
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), MAX_WIDTH, GAP),
|
||||||
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, firstLine+secondLine), maxW, marginW),
|
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, firstLine+secondLine), MAX_WIDTH, GAP),
|
||||||
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine+secondLine, ratiosCropped.size()), maxW, marginW)
|
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine+secondLine, ratiosCropped.size()), MAX_WIDTH, GAP)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looking for minimum difference between thumbs block height and maxH (may probably be little over)
|
// Looking for minimum difference between thumbs block height and maxHeight (may probably be little over)
|
||||||
|
final int realMaxHeight=Math.min(MAX_HEIGHT, MAX_WIDTH);
|
||||||
int[] optConf=null;
|
int[] optConf=null;
|
||||||
float optDiff=0;
|
float optDiff=0;
|
||||||
for(int[] conf : tries.keySet()){
|
for(int[] conf : tries.keySet()){
|
||||||
float[] heights=tries.get(conf);
|
float[] heights=tries.get(conf);
|
||||||
float confH=marginH*(heights.length-1);
|
float confH=GAP*(heights.length-1);
|
||||||
for(float h : heights) confH+=h;
|
for(float h : heights) confH+=h;
|
||||||
float confDiff=Math.abs(confH-maxH);
|
float confDiff=Math.abs(confH-realMaxHeight);
|
||||||
if(conf.length>1){
|
if(conf.length>1){
|
||||||
if(conf[0]>conf[1] || conf.length>2 && conf[1]>conf[2]){
|
if(conf[0]>conf[1] || conf.length>2 && conf[1]>conf[2]){
|
||||||
confDiff*=1.1;
|
confDiff*=1.1;
|
||||||
@@ -222,7 +240,7 @@ public class PhotoLayoutHelper{
|
|||||||
float[] optHeights=tries.get(optConf);
|
float[] optHeights=tries.get(optConf);
|
||||||
int k=0;
|
int k=0;
|
||||||
|
|
||||||
result.width=Math.round(maxW);
|
result.width=MAX_WIDTH;
|
||||||
result.rowSizes=new int[optHeights.length];
|
result.rowSizes=new int[optHeights.length];
|
||||||
result.tiles=new TiledLayoutResult.Tile[thumbs.size()];
|
result.tiles=new TiledLayoutResult.Tile[thumbs.size()];
|
||||||
float totalHeight=0f;
|
float totalHeight=0f;
|
||||||
@@ -240,11 +258,11 @@ public class PhotoLayoutHelper{
|
|||||||
ArrayList<TiledLayoutResult.Tile> row=new ArrayList<>();
|
ArrayList<TiledLayoutResult.Tile> row=new ArrayList<>();
|
||||||
for(int j=0; j<lineThumbs.size(); j++){
|
for(int j=0; j<lineThumbs.size(); j++){
|
||||||
float thumb_ratio=ratiosRemain.remove(0);
|
float thumb_ratio=ratiosRemain.remove(0);
|
||||||
float w=j==lineThumbs.size()-1 ? (maxW-totalWidth) : (thumb_ratio*lineHeight);
|
float w=j==lineThumbs.size()-1 ? (MAX_WIDTH-totalWidth) : (thumb_ratio*lineHeight);
|
||||||
totalWidth+=Math.round(w);
|
totalWidth+=Math.round(w);
|
||||||
if(j<lineThumbs.size()-1 && !gridLineOffsets.contains(totalWidth))
|
if(j<lineThumbs.size()-1 && !gridLineOffsets.contains(totalWidth))
|
||||||
gridLineOffsets.add(totalWidth);
|
gridLineOffsets.add(totalWidth);
|
||||||
TiledLayoutResult.Tile tile=new TiledLayoutResult.Tile(1, 1, w, lineHeight, 0, i);
|
TiledLayoutResult.Tile tile=new TiledLayoutResult.Tile(1, 1, 0, i, Math.round(w));
|
||||||
result.tiles[k]=tile;
|
result.tiles[k]=tile;
|
||||||
row.add(tile);
|
row.add(tile);
|
||||||
k++;
|
k++;
|
||||||
@@ -252,7 +270,7 @@ public class PhotoLayoutHelper{
|
|||||||
rowTiles.add(row);
|
rowTiles.add(row);
|
||||||
}
|
}
|
||||||
Collections.sort(gridLineOffsets);
|
Collections.sort(gridLineOffsets);
|
||||||
gridLineOffsets.add(Math.round(maxW));
|
gridLineOffsets.add(Math.round(MAX_WIDTH));
|
||||||
result.columnSizes=new int[gridLineOffsets.size()];
|
result.columnSizes=new int[gridLineOffsets.size()];
|
||||||
result.columnSizes[0]=gridLineOffsets.get(0);
|
result.columnSizes[0]=gridLineOffsets.get(0);
|
||||||
for(int i=gridLineOffsets.size()-1; i>0; i--){
|
for(int i=gridLineOffsets.size()-1; i>0; i--){
|
||||||
@@ -276,7 +294,7 @@ public class PhotoLayoutHelper{
|
|||||||
columnOffset+=tile.colSpan;
|
columnOffset+=tile.colSpan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.height=Math.round(totalHeight+marginH*(optHeights.length-1));
|
result.height=Math.round(totalHeight+GAP*(optHeights.length-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -310,19 +328,19 @@ public class PhotoLayoutHelper{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Tile{
|
public static class Tile{
|
||||||
public int colSpan, rowSpan, width, height, startCol, startRow;
|
public int colSpan, rowSpan, startCol, startRow;
|
||||||
|
public int width;
|
||||||
|
|
||||||
public Tile(int colSpan, int rowSpan, int width, int height, int startCol, int startRow){
|
public Tile(int colSpan, int rowSpan, int startCol, int startRow){
|
||||||
this.colSpan=colSpan;
|
this.colSpan=colSpan;
|
||||||
this.rowSpan=rowSpan;
|
this.rowSpan=rowSpan;
|
||||||
this.width=width;
|
|
||||||
this.height=height;
|
|
||||||
this.startCol=startCol;
|
this.startCol=startCol;
|
||||||
this.startRow=startRow;
|
this.startRow=startRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tile(int colSpan, int rowSpan, float width, float height, int startCol, int startRow){
|
public Tile(int colSpan, int rowSpan, int startCol, int startRow, int width){
|
||||||
this(colSpan, rowSpan, Math.round(width), Math.round(height), startCol, startRow);
|
this(colSpan, rowSpan, startCol, startRow);
|
||||||
|
this.width=width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -330,8 +348,8 @@ public class PhotoLayoutHelper{
|
|||||||
return "Tile{"+
|
return "Tile{"+
|
||||||
"colSpan="+colSpan+
|
"colSpan="+colSpan+
|
||||||
", rowSpan="+rowSpan+
|
", rowSpan="+rowSpan+
|
||||||
", width="+width+
|
", startCol="+startCol+
|
||||||
", height="+height+
|
", startRow="+startRow+
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
|
|||||||
followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||||
followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||||
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||||
|
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
|
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||||
relationship=item.parentFragment.getRelationship(item.account.id);
|
relationship=item.parentFragment.getRelationship(item.account.id);
|
||||||
if(item.notification.type == Notification.Type.FOLLOW_REQUEST && (relationship == null || !relationship.followedBy)){
|
if(item.notification.type == Notification.Type.FOLLOW_REQUEST && (relationship == null || !relationship.followedBy)){
|
||||||
actionWrap.setVisibility(View.GONE);
|
actionWrap.setVisibility(View.GONE);
|
||||||
|
|||||||
@@ -33,12 +33,14 @@ import me.grishka.appkit.Nav;
|
|||||||
|
|
||||||
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||||
public final Status status;
|
public final Status status;
|
||||||
|
public final String accountID;
|
||||||
|
|
||||||
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||||
|
|
||||||
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, String accountID, Status status){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
this.status=status;
|
this.status=status;
|
||||||
|
this.accountID=accountID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -72,7 +74,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public void onBind(ExtendedFooterStatusDisplayItem item){
|
public void onBind(ExtendedFooterStatusDisplayItem item){
|
||||||
Status s=item.status;
|
Status s=item.status;
|
||||||
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
|
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
|
||||||
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int)(s.reblogsCount%1000), s.reblogsCount));
|
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
|
||||||
|
reblogs.setVisibility(s.isReblogPermitted(item.accountID) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if(s.editedAt!=null){
|
if(s.editedAt!=null){
|
||||||
editHistory.setVisibility(View.VISIBLE);
|
editHistory.setVisibility(View.VISIBLE);
|
||||||
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));
|
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.app.Dialog;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -17,7 +16,6 @@ import android.view.animation.AlphaAnimation;
|
|||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
import org.joinmastodon.android.GlobalUserPreferences;
|
||||||
@@ -56,8 +54,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||||
private final TextView reply, boost, favorite, bookmark;
|
private final TextView replies, boosts, favorites;
|
||||||
private final ImageView share;
|
private final View reply, boost, favorite, share, bookmark;
|
||||||
private static final Animation opacityOut, opacityIn;
|
private static final Animation opacityOut, opacityIn;
|
||||||
|
|
||||||
private View touchingView = null;
|
private View touchingView = null;
|
||||||
@@ -91,22 +89,16 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
super(activity, R.layout.display_item_footer, parent);
|
super(activity, R.layout.display_item_footer, parent);
|
||||||
reply=findViewById(R.id.reply);
|
replies=findViewById(R.id.reply);
|
||||||
boost=findViewById(R.id.boost);
|
boosts=findViewById(R.id.boost);
|
||||||
favorite=findViewById(R.id.favorite);
|
favorites=findViewById(R.id.favorite);
|
||||||
bookmark=findViewById(R.id.bookmark);
|
|
||||||
share=findViewById(R.id.share);
|
reply=findViewById(R.id.reply_btn);
|
||||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
boost=findViewById(R.id.boost_btn);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
favorite=findViewById(R.id.favorite_btn);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
share=findViewById(R.id.share_btn);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
|
bookmark=findViewById(R.id.bookmark_btn);
|
||||||
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
|
|
||||||
}
|
|
||||||
View reply=findViewById(R.id.reply_btn);
|
|
||||||
View boost=findViewById(R.id.boost_btn);
|
|
||||||
View favorite=findViewById(R.id.favorite_btn);
|
|
||||||
View share=findViewById(R.id.share_btn);
|
|
||||||
View bookmark=findViewById(R.id.bookmark_btn);
|
|
||||||
reply.setOnTouchListener(this::onButtonTouch);
|
reply.setOnTouchListener(this::onButtonTouch);
|
||||||
reply.setOnClickListener(this::onReplyClick);
|
reply.setOnClickListener(this::onReplyClick);
|
||||||
reply.setOnLongClickListener(this::onReplyLongClick);
|
reply.setOnLongClickListener(this::onReplyLongClick);
|
||||||
@@ -131,33 +123,32 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(FooterStatusDisplayItem item){
|
public void onBind(FooterStatusDisplayItem item){
|
||||||
bindButton(reply, item.status.repliesCount);
|
bindText(replies, item.status.repliesCount);
|
||||||
bindButton(boost, item.status.reblogsCount);
|
bindText(boosts, item.status.reblogsCount);
|
||||||
bindButton(favorite, item.status.favouritesCount);
|
bindText(favorites, item.status.favouritesCount);
|
||||||
// in thread view, direct descendant posts display one direct reply to themselves,
|
// in thread view, direct descendant posts display one direct reply to themselves,
|
||||||
// hence in that case displaying whether there is another reply
|
// hence in that case displaying whether there is another reply
|
||||||
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor() ? 0 : 1;
|
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
|
||||||
reply.setSelected(item.status.repliesCount > compareTo);
|
reply.setSelected(item.status.repliesCount > compareTo);
|
||||||
boost.setSelected(item.status.reblogged);
|
boost.setSelected(item.status.reblogged);
|
||||||
favorite.setSelected(item.status.favourited);
|
favorite.setSelected(item.status.favourited);
|
||||||
bookmark.setSelected(item.status.bookmarked);
|
bookmark.setSelected(item.status.bookmarked);
|
||||||
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|
boost.setEnabled(item.status.isReblogPermitted(item.accountID));
|
||||||
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
|
|
||||||
|
|
||||||
int nextPos = getAbsoluteAdapterPosition() + 1;
|
int nextPos = getAbsoluteAdapterPosition() + 1;
|
||||||
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
|
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
|
||||||
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
|
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
|
||||||
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor() &&
|
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor &&
|
||||||
!nextIsWarning;
|
!nextIsWarning;
|
||||||
|
|
||||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
|
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
|
||||||
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
|
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
|
||||||
condenseBottom ? V.dp(-8) : 0);
|
condenseBottom ? V.dp(-5) : 0);
|
||||||
|
|
||||||
itemView.requestLayout();
|
itemView.requestLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindButton(TextView btn, long count){
|
private void bindText(TextView btn, long count){
|
||||||
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
|
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
|
||||||
btn.setText(UiUtils.abbreviateNumber(count));
|
btn.setText(UiUtils.abbreviateNumber(count));
|
||||||
btn.setCompoundDrawablePadding(V.dp(8));
|
btn.setCompoundDrawablePadding(V.dp(8));
|
||||||
@@ -181,8 +172,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
} else if (action == MotionEvent.ACTION_DOWN) {
|
} else if (action == MotionEvent.ACTION_DOWN) {
|
||||||
longClickPerformed = false;
|
longClickPerformed = false;
|
||||||
touchingView = v;
|
touchingView = v;
|
||||||
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
|
v.setPivotX(V.sp(28));
|
||||||
v.setPivotX(V.dp(20));
|
|
||||||
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
|
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
|
||||||
if (disabled) return true;
|
if (disabled) return true;
|
||||||
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
|
||||||
@@ -226,7 +216,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
|
|
||||||
private void boostConsumer(View v, Status r) {
|
private void boostConsumer(View v, Status r) {
|
||||||
v.startAnimation(opacityIn);
|
v.startAnimation(opacityIn);
|
||||||
bindButton(boost, r.reblogsCount);
|
bindText(boosts, r.reblogsCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean onBoostLongClick(View v){
|
private boolean onBoostLongClick(View v){
|
||||||
@@ -315,7 +305,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||||||
favorite.setSelected(!item.status.favourited);
|
favorite.setSelected(!item.status.favourited);
|
||||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
||||||
v.startAnimation(opacityIn);
|
v.startAnimation(opacityIn);
|
||||||
bindButton(favorite, r.favouritesCount);
|
bindText(favorites, r.favouritesCount);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
args.putBoolean("navigateToStatus", true);
|
args.putBoolean("navigateToStatus", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!redraft && TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
|
boolean isPixelfed = item.parentFragment.isInstancePixelfed();
|
||||||
|
boolean textEmpty = TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText);
|
||||||
|
if(!redraft && (isPixelfed || textEmpty)){
|
||||||
|
// pixelfed doesn't support /statuses/:id/source :/
|
||||||
|
if (isPixelfed) {
|
||||||
|
args.putString("sourceText", HtmlParser.text(item.status.content));
|
||||||
|
args.putString("sourceSpoiler", item.status.spoilerText);
|
||||||
|
}
|
||||||
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
|
||||||
}else if(item.scheduledStatus!=null){
|
}else if(item.scheduledStatus!=null){
|
||||||
args.putString("sourceText", item.status.text);
|
args.putString("sourceText", item.status.text);
|
||||||
@@ -392,6 +399,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||||||
else collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start();
|
else collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start();
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemView.setPaddingRelative(itemView.getPaddingStart(), itemView.getPaddingTop(),
|
||||||
|
item.inset ? V.dp(10) : V.dp(4), itemView.getPaddingBottom());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.app.Activity;
|
|||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewTreeObserver;
|
import android.view.ViewTreeObserver;
|
||||||
@@ -25,7 +26,9 @@ import org.joinmastodon.android.model.Status;
|
|||||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild;
|
import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild;
|
||||||
|
import org.joinmastodon.android.ui.views.MaxWidthFrameLayout;
|
||||||
import org.joinmastodon.android.ui.views.MediaGridLayout;
|
import org.joinmastodon.android.ui.views.MediaGridLayout;
|
||||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||||
|
|
||||||
@@ -88,6 +91,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
private final View.OnClickListener clickListener=this::onViewClick, altTextClickListener=this::onAltTextClick;
|
private final View.OnClickListener clickListener=this::onViewClick, altTextClickListener=this::onAltTextClick;
|
||||||
private final ArrayList<MediaAttachmentViewController> controllers=new ArrayList<>();
|
private final ArrayList<MediaAttachmentViewController> controllers=new ArrayList<>();
|
||||||
|
|
||||||
|
private final MaxWidthFrameLayout overlays;
|
||||||
private final FrameLayout altTextWrapper;
|
private final FrameLayout altTextWrapper;
|
||||||
private final TextView altTextButton;
|
private final TextView altTextButton;
|
||||||
private final ImageView noAltTextButton;
|
private final ImageView noAltTextButton;
|
||||||
@@ -105,8 +109,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
wrapper=(FrameLayout)itemView;
|
wrapper=(FrameLayout)itemView;
|
||||||
layout=new MediaGridLayout(activity);
|
layout=new MediaGridLayout(activity);
|
||||||
wrapper.addView(layout);
|
wrapper.addView(layout);
|
||||||
|
wrapper.setClipToPadding(false);
|
||||||
|
|
||||||
activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, wrapper);
|
overlays=new MaxWidthFrameLayout(activity);
|
||||||
|
overlays.setMaxWidth(UiUtils.MAX_WIDTH);
|
||||||
|
wrapper.addView(overlays, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER_HORIZONTAL));
|
||||||
|
|
||||||
|
activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, overlays);
|
||||||
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
||||||
altTextButton=findViewById(R.id.alt_button);
|
altTextButton=findViewById(R.id.alt_button);
|
||||||
noAltTextButton=findViewById(R.id.no_alt_button);
|
noAltTextButton=findViewById(R.id.no_alt_button);
|
||||||
@@ -215,7 +224,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
int[] loc={0, 0};
|
int[] loc={0, 0};
|
||||||
v.getLocationInWindow(loc);
|
v.getLocationInWindow(loc);
|
||||||
int btnL=loc[0], btnT=loc[1];
|
int btnL=loc[0], btnT=loc[1];
|
||||||
wrapper.getLocationInWindow(loc);
|
overlays.getLocationInWindow(loc);
|
||||||
btnL-=loc[0];
|
btnL-=loc[0];
|
||||||
btnT-=loc[1];
|
btnT-=loc[1];
|
||||||
|
|
||||||
@@ -278,7 +287,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
|||||||
int[] loc={0, 0};
|
int[] loc={0, 0};
|
||||||
btn.getLocationInWindow(loc);
|
btn.getLocationInWindow(loc);
|
||||||
int btnL=loc[0], btnT=loc[1];
|
int btnL=loc[0], btnT=loc[1];
|
||||||
wrapper.getLocationInWindow(loc);
|
overlays.getLocationInWindow(loc);
|
||||||
btnL-=loc[0];
|
btnL-=loc[0];
|
||||||
btnT-=loc[1];
|
btnT-=loc[1];
|
||||||
|
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
|||||||
public void updateVisibility(StatusPrivacy visibility) {
|
public void updateVisibility(StatusPrivacy visibility) {
|
||||||
this.visibility = visibility;
|
this.visibility = visibility;
|
||||||
this.iconEnd = visibility != null ? switch (visibility) {
|
this.iconEnd = visibility != null ? switch (visibility) {
|
||||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
case PUBLIC -> R.drawable.ic_fluent_earth_20sp_regular;
|
||||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
|
case UNLISTED -> R.drawable.ic_fluent_lock_open_20sp_regular;
|
||||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
|
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20sp_filled;
|
||||||
default -> 0;
|
default -> 0;
|
||||||
} : 0;
|
} : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,29 +47,20 @@ public abstract class StatusDisplayItem{
|
|||||||
public final BaseStatusListFragment parentFragment;
|
public final BaseStatusListFragment parentFragment;
|
||||||
public boolean inset;
|
public boolean inset;
|
||||||
public int index;
|
public int index;
|
||||||
private ThreadFragment.NeighborAncestryInfo ancestryInfo;
|
|
||||||
public boolean
|
public boolean
|
||||||
|
hasDescendantNeighbor = false,
|
||||||
|
hasAncestoringNeighbor = false,
|
||||||
isMainStatus = true,
|
isMainStatus = true,
|
||||||
isDirectDescendant = false;
|
isDirectDescendant = false;
|
||||||
|
|
||||||
public boolean hasDescendantNeighbor() {
|
|
||||||
return Optional.ofNullable(ancestryInfo)
|
|
||||||
.map(ThreadFragment.NeighborAncestryInfo::hasDescendantNeighbor)
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasAncestoringNeighbor() {
|
|
||||||
return Optional.ofNullable(ancestryInfo)
|
|
||||||
.map(ThreadFragment.NeighborAncestryInfo::hasAncestoringNeighbor)
|
|
||||||
.orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAncestryInfo(
|
public void setAncestryInfo(
|
||||||
ThreadFragment.NeighborAncestryInfo ancestryInfo,
|
boolean hasDescendantNeighbor,
|
||||||
|
boolean hasAncestoringNeighbor,
|
||||||
boolean isMainStatus,
|
boolean isMainStatus,
|
||||||
boolean isDirectDescendant
|
boolean isDirectDescendant
|
||||||
) {
|
) {
|
||||||
this.ancestryInfo = ancestryInfo;
|
this.hasDescendantNeighbor = hasDescendantNeighbor;
|
||||||
|
this.hasAncestoringNeighbor = hasAncestoringNeighbor;
|
||||||
this.isMainStatus = isMainStatus;
|
this.isMainStatus = isMainStatus;
|
||||||
this.isDirectDescendant = isDirectDescendant;
|
this.isDirectDescendant = isDirectDescendant;
|
||||||
}
|
}
|
||||||
@@ -114,6 +105,21 @@ public abstract class StatusDisplayItem{
|
|||||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) {
|
||||||
|
String parentID = parent.getID();
|
||||||
|
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
|
||||||
|
: account == null ? fragment.getString(R.string.sk_in_reply)
|
||||||
|
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
|
||||||
|
: fragment.getString(R.string.in_reply_to, account.displayName);
|
||||||
|
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
|
||||||
|
: account == null ? fragment.getString(R.string.sk_in_reply)
|
||||||
|
: fragment.getString(R.string.in_reply_to, account.displayName);
|
||||||
|
return new ReblogOrReplyLineStatusDisplayItem(
|
||||||
|
parentID, fragment, text, account == null ? List.of() : account.emojis,
|
||||||
|
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
|
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
|
||||||
String parentID=parentObject.getID();
|
String parentID=parentObject.getID();
|
||||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||||
@@ -129,24 +135,14 @@ public abstract class StatusDisplayItem{
|
|||||||
|
|
||||||
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
||||||
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
|
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
|
||||||
String text = threadReply ? fragment.getString(R.string.sk_show_thread)
|
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
|
||||||
: account == null ? fragment.getString(R.string.sk_in_reply)
|
|
||||||
: GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName
|
|
||||||
: fragment.getString(R.string.in_reply_to, account.displayName);
|
|
||||||
String fullText = threadReply ? fragment.getString(R.string.sk_show_thread)
|
|
||||||
: account == null ? fragment.getString(R.string.sk_in_reply)
|
|
||||||
: fragment.getString(R.string.in_reply_to, account.displayName);
|
|
||||||
replyLine = new ReblogOrReplyLineStatusDisplayItem(
|
|
||||||
parentID, fragment, text, account == null ? List.of() : account.emojis,
|
|
||||||
R.drawable.ic_fluent_arrow_reply_20_filled, null, null, fullText
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(status.reblog!=null){
|
if(status.reblog!=null){
|
||||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||||
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
|
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
|
||||||
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
|
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
|
||||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
|
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||||
}, fullText));
|
}, fullText));
|
||||||
@@ -161,7 +157,7 @@ public abstract class StatusDisplayItem{
|
|||||||
// post contains a hashtag the user is following
|
// post contains a hashtag the user is following
|
||||||
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
||||||
parentID, fragment, hashtag.name, List.of(),
|
parentID, fragment, hashtag.name, List.of(),
|
||||||
R.drawable.ic_fluent_number_symbol_20_filled, null,
|
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
|
||||||
i -> {
|
i -> {
|
||||||
args.putString("hashtag", hashtag.name);
|
args.putString("hashtag", hashtag.name);
|
||||||
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
|
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
spoilerEmojiHelper.setText(parsedSpoilerText);
|
spoilerEmojiHelper.setText(parsedSpoilerText);
|
||||||
}
|
}
|
||||||
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
|
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
|
||||||
UiUtils.loadMaxWidth(parentFragment.getContext());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTranslationShown(boolean translationShown) {
|
public void setTranslationShown(boolean translationShown) {
|
||||||
@@ -238,13 +237,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
|||||||
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
|
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
// remove additional padding when (transparently padded) translate button is visible
|
// remove additional padding when (transparently padded) translate button is visible
|
||||||
int pos = getAbsoluteAdapterPosition();
|
int nextPos = getAbsoluteAdapterPosition() + 1;
|
||||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(),
|
boolean nextIsFooter = item.parentFragment.getDisplayItems().size() > nextPos &&
|
||||||
(translateVisible &&
|
item.parentFragment.getDisplayItems().get(nextPos) instanceof FooterStatusDisplayItem;
|
||||||
item.parentFragment.getDisplayItems().size() >= pos + 1 &&
|
int bottomPadding = (translateVisible && nextIsFooter) ? 0
|
||||||
item.parentFragment.getDisplayItems().get(pos + 1) instanceof FooterStatusDisplayItem)
|
: nextIsFooter ? V.dp(6)
|
||||||
? 0 : V.dp(12)
|
: V.dp(12);
|
||||||
);
|
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
|
||||||
|
|
||||||
if (!GlobalUserPreferences.collapseLongPosts) {
|
if (!GlobalUserPreferences.collapseLongPosts) {
|
||||||
textScrollView.setLayoutParams(wrapParams);
|
textScrollView.setLayoutParams(wrapParams);
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.CornerPathEffect;
|
import android.graphics.CornerPathEffect;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.Rect;
|
import android.os.Build;
|
||||||
import android.graphics.RectF;
|
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.view.GestureDetector;
|
import android.view.GestureDetector;
|
||||||
@@ -33,7 +32,8 @@ public class ClickableLinksDelegate {
|
|||||||
hlPaint=new Paint();
|
hlPaint=new Paint();
|
||||||
hlPaint.setAntiAlias(true);
|
hlPaint.setAntiAlias(true);
|
||||||
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
||||||
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light));
|
hlPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
hlPaint.setStrokeWidth(V.dp(4));
|
||||||
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
|
gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ public class ClickableLinksDelegate {
|
|||||||
public void onDraw(Canvas canvas){
|
public void onDraw(Canvas canvas){
|
||||||
if(hlPath!=null){
|
if(hlPath!=null){
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.translate(0, view.getPaddingTop());
|
canvas.translate(view.getTotalPaddingLeft(), view.getTotalPaddingTop());
|
||||||
canvas.drawPath(hlPath, hlPaint);
|
canvas.drawPath(hlPath, hlPaint);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
@@ -73,59 +73,29 @@ public class ClickableLinksDelegate {
|
|||||||
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
|
private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||||
@Override
|
@Override
|
||||||
public boolean onDown(@NonNull MotionEvent event) {
|
public boolean onDown(@NonNull MotionEvent event) {
|
||||||
int line=-1;
|
int padLeft=view.getTotalPaddingLeft(), padRight=view.getTotalPaddingRight(), padTop=view.getTotalPaddingTop(), padBottom=view.getTotalPaddingBottom();
|
||||||
Rect rect=new Rect();
|
float x=event.getX(), y=event.getY();
|
||||||
Layout l=view.getLayout();
|
if(x<padLeft || y<padTop || x>view.getWidth()-padRight || y>view.getHeight()-padBottom)
|
||||||
for(int i=0;i<l.getLineCount();i++){
|
|
||||||
view.getLineBounds(i, rect);
|
|
||||||
if(rect.contains((int)event.getX(), (int)event.getY())){
|
|
||||||
line=i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(line==-1){
|
|
||||||
return false;
|
return false;
|
||||||
}
|
x-=padLeft;
|
||||||
|
y-=padTop;
|
||||||
|
Layout l=view.getLayout();
|
||||||
|
int line=l.getLineForVertical(Math.round(y));
|
||||||
|
int position=l.getOffsetForHorizontal(line, x);
|
||||||
|
|
||||||
CharSequence text=view.getText();
|
CharSequence text=view.getText();
|
||||||
if(text instanceof Spanned s){
|
if(text instanceof Spanned s){
|
||||||
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
|
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
|
||||||
if(spans.length>0){
|
for(LinkSpan span:spans){
|
||||||
for(LinkSpan span:spans){
|
int start=s.getSpanStart(span);
|
||||||
int start=s.getSpanStart(span);
|
int end=s.getSpanEnd(span);
|
||||||
int end=s.getSpanEnd(span);
|
if(start<=position && end>position){
|
||||||
int lstart=l.getLineForOffset(start);
|
selectedSpan=span;
|
||||||
int lend=l.getLineForOffset(end);
|
hlPath=new Path();
|
||||||
if(line>=lstart && line<=lend){
|
l.getSelectionPath(start, end, hlPath);
|
||||||
if(line==lstart && event.getX()-view.getPaddingLeft()<l.getPrimaryHorizontal(start)){
|
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
|
||||||
continue;
|
view.invalidate();
|
||||||
}
|
return true;
|
||||||
if(line==lend && event.getX()-view.getPaddingLeft()>l.getPrimaryHorizontal(end)){
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
hlPath=new Path();
|
|
||||||
selectedSpan=span;
|
|
||||||
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
|
|
||||||
//l.getSelectionPath(start, end, hlPath);
|
|
||||||
for(int j=lstart;j<=lend;j++){
|
|
||||||
Rect bounds=new Rect();
|
|
||||||
l.getLineBounds(j, bounds);
|
|
||||||
//bounds.left+=view.getPaddingLeft();
|
|
||||||
if(j==lstart){
|
|
||||||
bounds.left=Math.round(l.getPrimaryHorizontal(start));
|
|
||||||
}
|
|
||||||
if(j==lend){
|
|
||||||
bounds.right=Math.round(l.getPrimaryHorizontal(end));
|
|
||||||
}else{
|
|
||||||
CharSequence lineChars=view.getText().subSequence(l.getLineStart(j), l.getLineEnd(j));
|
|
||||||
bounds.right=Math.round(view.getPaint().measureText(lineChars.toString()))/*+view.getPaddingRight()*/;
|
|
||||||
}
|
|
||||||
bounds.inset(V.dp(-2), V.dp(-2));
|
|
||||||
hlPath.addRect(new RectF(bounds), Path.Direction.CW);
|
|
||||||
}
|
|
||||||
hlPath.offset(view.getPaddingLeft(), 0);
|
|
||||||
view.invalidate();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package org.joinmastodon.android.ui.text;
|
package org.joinmastodon.android.ui.text;
|
||||||
|
|
||||||
import android.graphics.Typeface;
|
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.TextUtils;
|
||||||
import android.text.style.BackgroundColorSpan;
|
|
||||||
import android.text.style.BulletSpan;
|
import android.text.style.BulletSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
|
||||||
import android.text.style.LeadingMarginSpan;
|
import android.text.style.LeadingMarginSpan;
|
||||||
import android.text.style.RelativeSizeSpan;
|
import android.text.style.RelativeSizeSpan;
|
||||||
import android.text.style.StrikethroughSpan;
|
import android.text.style.StrikethroughSpan;
|
||||||
@@ -17,13 +13,10 @@ import android.text.style.SubscriptSpan;
|
|||||||
import android.text.style.SuperscriptSpan;
|
import android.text.style.SuperscriptSpan;
|
||||||
import android.text.style.TypefaceSpan;
|
import android.text.style.TypefaceSpan;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.twitter.twittertext.Regex;
|
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;
|
||||||
@@ -251,6 +244,10 @@ public class HtmlParser{
|
|||||||
return Jsoup.clean(html, Safelist.none());
|
return Jsoup.clean(html, Safelist.none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String text(String html) {
|
||||||
|
return Jsoup.parse(html).body().wholeText();
|
||||||
|
}
|
||||||
|
|
||||||
public static CharSequence parseLinks(String text){
|
public static CharSequence parseLinks(String text){
|
||||||
Matcher matcher=URL_PATTERN.matcher(text);
|
Matcher matcher=URL_PATTERN.matcher(text);
|
||||||
if(!matcher.find()) // Return the original string if there are no URLs
|
if(!matcher.find()) // Return the original string if there are no URLs
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import android.content.DialogInterface;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
@@ -37,6 +38,7 @@ import android.text.SpannableStringBuilder;
|
|||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@@ -54,6 +56,8 @@ import org.joinmastodon.android.E;
|
|||||||
import org.joinmastodon.android.GlobalUserPreferences;
|
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.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||||
import org.joinmastodon.android.api.StatusInteractionController;
|
import org.joinmastodon.android.api.StatusInteractionController;
|
||||||
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
|
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
|
||||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||||
@@ -98,9 +102,9 @@ import org.parceler.Parcels;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.net.IDN;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
@@ -140,15 +144,11 @@ public class UiUtils {
|
|||||||
private static Handler mainHandler = new Handler(Looper.getMainLooper());
|
private static Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
|
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
|
||||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||||
public static int MAX_WIDTH;
|
public static int MAX_WIDTH, SCROLL_TO_TOP_DELTA;
|
||||||
|
|
||||||
private UiUtils() {
|
private UiUtils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadMaxWidth(Context ctx) {
|
|
||||||
if (MAX_WIDTH == 0) MAX_WIDTH = (int) ctx.getResources().getDimension(R.dimen.layout_max_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void launchWebBrowser(Context context, String url) {
|
public static void launchWebBrowser(Context context, String url) {
|
||||||
try {
|
try {
|
||||||
if (GlobalUserPreferences.useCustomTabs) {
|
if (GlobalUserPreferences.useCustomTabs) {
|
||||||
@@ -897,6 +897,10 @@ public class UiUtils {
|
|||||||
|
|
||||||
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
|
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
|
||||||
if (palette != null) palette.apply(context);
|
if (palette != null) palette.apply(context);
|
||||||
|
|
||||||
|
Resources res = context.getResources();
|
||||||
|
MAX_WIDTH = (int) res.getDimension(R.dimen.layout_max_width);
|
||||||
|
SCROLL_TO_TOP_DELTA = (int) res.getDimension(R.dimen.scroll_to_top_delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isDarkTheme() {
|
public static boolean isDarkTheme() {
|
||||||
@@ -905,6 +909,32 @@ public class UiUtils {
|
|||||||
return theme == GlobalUserPreferences.ThemePreference.DARK;
|
return theme == GlobalUserPreferences.ThemePreference.DARK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Optional<Pair<String, Optional<String>>> parseFediverseHandle(String maybeFediHandle) {
|
||||||
|
// https://stackoverflow.com/a/26987741, except i put a + here ... v
|
||||||
|
String domainRegex = "^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]\\.)+(xn--)?([a-z0-9][a-z0-9\\-]{0,60}|[a-z0-9-]{1,30}\\.[a-z]{2,})$";
|
||||||
|
if (maybeFediHandle.toLowerCase().startsWith("mailto:")) {
|
||||||
|
maybeFediHandle = maybeFediHandle.substring("mailto:".length());
|
||||||
|
}
|
||||||
|
List<String> parts = Arrays.stream(maybeFediHandle.split("@"))
|
||||||
|
.filter(part -> !part.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (parts.size() == 0 || !parts.get(0).matches("^[^/\\s]+$")) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else if (parts.size() == 2) {
|
||||||
|
try {
|
||||||
|
String domain = IDN.toASCII(parts.get(1));
|
||||||
|
if (!domain.matches(domainRegex)) return Optional.empty();
|
||||||
|
return Optional.of(Pair.create(parts.get(0), Optional.of(parts.get(1))));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
} else if (maybeFediHandle.startsWith("@")) {
|
||||||
|
return Optional.of(Pair.create(parts.get(0), Optional.empty()));
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://mastodon.foo.bar/@User
|
// https://mastodon.foo.bar/@User
|
||||||
// https://mastodon.foo.bar/@User/43456787654678
|
// https://mastodon.foo.bar/@User/43456787654678
|
||||||
// https://pleroma.foo.bar/users/User
|
// https://pleroma.foo.bar/users/User
|
||||||
@@ -921,7 +951,7 @@ public class UiUtils {
|
|||||||
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
|
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
|
||||||
//
|
//
|
||||||
// COPIED FROM https://github.com/tuskyapp/Tusky/blob/develop/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
|
// COPIED FROM https://github.com/tuskyapp/Tusky/blob/develop/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
|
||||||
public static boolean looksLikeMastodonUrl(String urlString) {
|
public static boolean looksLikeFediverseUrl(String urlString) {
|
||||||
URI uri;
|
URI uri;
|
||||||
try {
|
try {
|
||||||
uri = new URI(urlString);
|
uri = new URI(urlString);
|
||||||
@@ -1023,50 +1053,46 @@ public class UiUtils {
|
|||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
|
public static Optional<MastodonAPIRequest<SearchResults>> lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
|
||||||
lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
|
return lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
|
||||||
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
|
!results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
|
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
|
||||||
lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
|
return lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
|
||||||
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
|
!results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends Searchable> void lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
|
public static <T extends Searchable> Optional<MastodonAPIRequest<SearchResults>> lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer<T> resultConsumer, Function<SearchResults, Optional<T>> extractResult) {
|
||||||
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
|
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
|
||||||
resultConsumer.accept(query);
|
resultConsumer.accept(query);
|
||||||
return;
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
|
return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults results) {
|
public void onSuccess(SearchResults results) {
|
||||||
Optional<T> result = extractResult.apply(results);
|
Optional<T> result = extractResult.apply(results);
|
||||||
if (result.isPresent()) resultConsumer.accept(result.get());
|
if (result.isPresent()) resultConsumer.accept(result.get());
|
||||||
else {
|
else {
|
||||||
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||||
resultConsumer.accept(null);
|
resultConsumer.accept(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
error.showToast(context);
|
error.showToast(context);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress((Activity) context, R.string.loading, true,
|
.wrapProgress((Activity) context, R.string.loading, true,
|
||||||
d -> transformDialogForLookup(context, targetAccountID, null, d))
|
d -> transformDialogForLookup(context, targetAccountID, null, d))
|
||||||
.exec(targetAccountID);
|
.exec(targetAccountID));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void openURL(Context context, String accountID, String url) {
|
public static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
|
||||||
openURL(context, accountID, url, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
|
|
||||||
if (accountID != null) {
|
if (accountID != null) {
|
||||||
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
|
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
|
||||||
} else {
|
} else {
|
||||||
@@ -1081,19 +1107,97 @@ public class UiUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
|
private static Bundle bundleError(String error) {
|
||||||
lookupURL(context, accountID, url, launchBrowser, (clazz, args) -> {
|
Bundle args = new Bundle();
|
||||||
if (clazz == null) return;
|
args.putString("error", error);
|
||||||
Nav.go((Activity) context, clazz, args);
|
return args;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, BiConsumer<Class<? extends Fragment>, Bundle> go) {
|
private static Bundle bundleError(ErrorResponse error) {
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
if (error instanceof MastodonErrorResponse e) {
|
||||||
|
args.putString("error", e.error);
|
||||||
|
args.putInt("httpStatus", e.httpStatus);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openURL(Context context, String accountID, String url) {
|
||||||
|
openURL(context, accountID, url, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
|
||||||
|
lookupURL(context, accountID, url, (clazz, args) -> {
|
||||||
|
if (clazz == null) {
|
||||||
|
if (args != null && args.containsKey("error")) Toast.makeText(context, args.getString("error"), Toast.LENGTH_SHORT).show();
|
||||||
|
if (launchBrowser) launchWebBrowser(context, url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Nav.go((Activity) context, clazz, args);
|
||||||
|
}).map(req -> req.wrapProgress((Activity) context, R.string.loading, true, d ->
|
||||||
|
transformDialogForLookup(context, accountID, url, d)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean acctMatches(String accountID, String acct, String queriedUsername, @Nullable String queriedDomain) {
|
||||||
|
// check if the username matches
|
||||||
|
if (!acct.split("@")[0].equalsIgnoreCase(queriedUsername)) return false;
|
||||||
|
|
||||||
|
boolean resultOnHomeInstance = !acct.contains("@");
|
||||||
|
if (resultOnHomeInstance) {
|
||||||
|
// acct is formatted like 'someone'
|
||||||
|
// only allow home instance result if query didn't specify a domain,
|
||||||
|
// or the specified domain does, in fact, match the account session's domain
|
||||||
|
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
|
||||||
|
return queriedDomain == null || session.domain.equalsIgnoreCase(queriedDomain);
|
||||||
|
} else if (queriedDomain == null) {
|
||||||
|
// accept whatever result we have as there's no queried domain to compare to
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// acct is formatted like 'someone@somewhere'
|
||||||
|
return acct.split("@")[1].equalsIgnoreCase(queriedDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccountHandle(Context context, String accountID, String query, BiConsumer<Class<? extends Fragment>, Bundle> go) {
|
||||||
|
return parseFediverseHandle(query).map(
|
||||||
|
handle -> lookupAccountHandle(context, accountID, handle, go))
|
||||||
|
.or(() -> {
|
||||||
|
go.accept(null, null);
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public static MastodonAPIRequest<SearchResults> lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
|
||||||
|
String fullHandle = ("@" + queryHandle.first) + (queryHandle.second.map(domain -> "@" + domain).orElse(""));
|
||||||
|
return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
|
||||||
|
.setCallback(new Callback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(SearchResults results) {
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Optional<Account> account = results.accounts.stream()
|
||||||
|
.filter(a -> acctMatches(accountID, a.acct, queryHandle.first, queryHandle.second.orElse(null)))
|
||||||
|
.findAny();
|
||||||
|
if (account.isPresent()) {
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
|
||||||
|
go.accept(ProfileFragment.class, args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error) {
|
||||||
|
go.accept(null, bundleError(error));
|
||||||
|
}
|
||||||
|
}).exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<MastodonAPIRequest<?>> lookupURL(Context context, String accountID, String url, BiConsumer<Class<? extends Fragment>, Bundle> go) {
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url);
|
||||||
List<String> path = uri.getPathSegments();
|
List<String> path = uri.getPathSegments();
|
||||||
if (accountID != null && "https".equals(uri.getScheme())) {
|
if (accountID != null && "https".equals(uri.getScheme())) {
|
||||||
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
|
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
|
||||||
new GetStatusByID(path.get(1))
|
return Optional.of(new GetStatusByID(path.get(1))
|
||||||
.setCallback(new Callback<>() {
|
.setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Status result) {
|
public void onSuccess(Status result) {
|
||||||
@@ -1105,17 +1209,12 @@ public class UiUtils {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
error.showToast(context);
|
go.accept(null, bundleError(error));
|
||||||
if (launchBrowser) launchWebBrowser(context, url);
|
|
||||||
go.accept(null, null);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress((Activity) context, R.string.loading, true,
|
.exec(accountID));
|
||||||
d -> transformDialogForLookup(context, accountID, url, d))
|
} else if (looksLikeFediverseUrl(url)) {
|
||||||
.exec(accountID);
|
return Optional.of(new GetSearchResults(url, null, true)
|
||||||
return;
|
|
||||||
} else if (looksLikeMastodonUrl(url)) {
|
|
||||||
new GetSearchResults(url, null, true)
|
|
||||||
.setCallback(new Callback<>() {
|
.setCallback(new Callback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SearchResults results) {
|
public void onSuccess(SearchResults results) {
|
||||||
@@ -1133,26 +1232,19 @@ public class UiUtils {
|
|||||||
go.accept(ProfileFragment.class, args);
|
go.accept(ProfileFragment.class, args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (launchBrowser) launchWebBrowser(context, url);
|
|
||||||
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
|
||||||
go.accept(null, null);
|
go.accept(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error) {
|
public void onError(ErrorResponse error) {
|
||||||
error.showToast(context);
|
go.accept(null, bundleError(error));
|
||||||
if (launchBrowser) launchWebBrowser(context, url);
|
|
||||||
go.accept(null, null);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.wrapProgress((Activity) context, R.string.loading, true,
|
.exec(accountID));
|
||||||
d -> transformDialogForLookup(context, accountID, url, d))
|
|
||||||
.exec(accountID);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (launchBrowser) launchWebBrowser(context, url);
|
|
||||||
go.accept(null, null);
|
go.accept(null, null);
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void copyText(View v, String text) {
|
public static void copyText(View v, String text) {
|
||||||
@@ -1315,6 +1407,23 @@ public class UiUtils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void showFragmentForNotification(Context context, Notification n, String accountID, Bundle extras) {
|
||||||
|
if (extras == null) extras = new Bundle();
|
||||||
|
extras.putString("account", accountID);
|
||||||
|
if (n.status!=null) {
|
||||||
|
Status status=n.status;
|
||||||
|
extras.putParcelable("status", Parcels.wrap(status.clone()));
|
||||||
|
Nav.go((Activity) context, ThreadFragment.class, extras);
|
||||||
|
} else if (n.report != null) {
|
||||||
|
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||||
|
UiUtils.launchWebBrowser(context, "https://"+domain+"/admin/reports/"+n.report.id);
|
||||||
|
} else if (n.account != null) {
|
||||||
|
extras.putString("account", accountID);
|
||||||
|
extras.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||||
|
Nav.go((Activity) context, ProfileFragment.class, extras);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a View.OnClickListener to filter multiple clicks in succession.
|
* Wraps a View.OnClickListener to filter multiple clicks in succession.
|
||||||
* Useful for buttons that perform some action that changes their state asynchronously.
|
* Useful for buttons that perform some action that changes their state asynchronously.
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ public class ComposeMediaLayout extends ViewGroup{
|
|||||||
|
|
||||||
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
|
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
UiUtils.loadMaxWidth(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package org.joinmastodon.android.ui.views;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
|
||||||
public class MaxWidthFrameLayout extends FrameLayout{
|
public class MaxWidthFrameLayout extends FrameLayout{
|
||||||
private int maxWidth;
|
private int maxWidth, defaultWidth;
|
||||||
|
|
||||||
public MaxWidthFrameLayout(Context context){
|
public MaxWidthFrameLayout(Context context){
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@@ -22,6 +23,7 @@ public class MaxWidthFrameLayout extends FrameLayout{
|
|||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MaxWidthFrameLayout);
|
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MaxWidthFrameLayout);
|
||||||
maxWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_android_maxWidth, Integer.MAX_VALUE);
|
maxWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_android_maxWidth, Integer.MAX_VALUE);
|
||||||
|
defaultWidth=ta.getDimensionPixelSize(R.styleable.MaxWidthFrameLayout_defaultWidth, -1);
|
||||||
ta.recycle();
|
ta.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,10 +35,19 @@ public class MaxWidthFrameLayout extends FrameLayout{
|
|||||||
this.maxWidth=maxWidth;
|
this.maxWidth=maxWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getDefaultWidth() {
|
||||||
|
return defaultWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultWidth(int defaultWidth) {
|
||||||
|
this.defaultWidth = defaultWidth;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||||
if(MeasureSpec.getSize(widthMeasureSpec)>maxWidth){
|
if(MeasureSpec.getSize(widthMeasureSpec)>maxWidth){
|
||||||
widthMeasureSpec=maxWidth | MeasureSpec.getMode(widthMeasureSpec);
|
int width = defaultWidth >= 0 ? defaultWidth : maxWidth;
|
||||||
|
widthMeasureSpec=width | MeasureSpec.getMode(widthMeasureSpec);
|
||||||
}
|
}
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import me.grishka.appkit.utils.V;
|
|||||||
public class MediaGridLayout extends ViewGroup{
|
public class MediaGridLayout extends ViewGroup{
|
||||||
private static final String TAG="MediaGridLayout";
|
private static final String TAG="MediaGridLayout";
|
||||||
|
|
||||||
private static final int GAP=1; // dp
|
private static final int GAP=2; // dp
|
||||||
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||||
private int[] columnStarts=new int[10], columnEnds=new int[10], rowStarts=new int[10], rowEnds=new int[10];
|
private int[] columnStarts=new int[10], columnEnds=new int[10], rowStarts=new int[10], rowEnds=new int[10];
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ public class MediaGridLayout extends ViewGroup{
|
|||||||
|
|
||||||
public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){
|
public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){
|
||||||
super(context, attrs, defStyle);
|
super(context, attrs, defStyle);
|
||||||
UiUtils.loadMaxWidth(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -38,6 +37,9 @@ public class MediaGridLayout extends ViewGroup{
|
|||||||
}
|
}
|
||||||
int width=Math.min(UiUtils.MAX_WIDTH, MeasureSpec.getSize(widthMeasureSpec));
|
int width=Math.min(UiUtils.MAX_WIDTH, MeasureSpec.getSize(widthMeasureSpec));
|
||||||
int height=Math.round(width*(tiledLayout.height/(float)PhotoLayoutHelper.MAX_WIDTH));
|
int height=Math.round(width*(tiledLayout.height/(float)PhotoLayoutHelper.MAX_WIDTH));
|
||||||
|
if(tiledLayout.width<PhotoLayoutHelper.MAX_WIDTH){
|
||||||
|
width=Math.round(width*(tiledLayout.width/(float)PhotoLayoutHelper.MAX_WIDTH));
|
||||||
|
}
|
||||||
|
|
||||||
int offset=0;
|
int offset=0;
|
||||||
for(int i=0;i<tiledLayout.columnSizes.length;i++){
|
for(int i=0;i<tiledLayout.columnSizes.length;i++){
|
||||||
@@ -74,9 +76,13 @@ public class MediaGridLayout extends ViewGroup{
|
|||||||
if(tiledLayout==null)
|
if(tiledLayout==null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
int maxWidth=UiUtils.MAX_WIDTH;
|
||||||
|
if(tiledLayout.width<PhotoLayoutHelper.MAX_WIDTH){
|
||||||
|
maxWidth=Math.round((r-l)*(tiledLayout.width/(float)PhotoLayoutHelper.MAX_WIDTH));
|
||||||
|
}
|
||||||
int xOffset=0;
|
int xOffset=0;
|
||||||
if(r-l>UiUtils.MAX_WIDTH){
|
if(r-l>maxWidth){
|
||||||
xOffset=(r-l)/2-UiUtils.MAX_WIDTH/2;
|
xOffset=(r-l)/2-maxWidth/2;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(int i=0;i<getChildCount();i++){
|
for(int i=0;i<getChildCount();i++){
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,5 +2,5 @@
|
|||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="?android:colorAccent" 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="?colorIconDisabled" />
|
||||||
</selector>
|
</selector>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<item android:drawable="@drawable/ic_fluent_megaphone_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
<item android:drawable="@drawable/ic_fluent_megaphone_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
||||||
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
||||||
<shape android:shape="oval">
|
<shape android:shape="oval">
|
||||||
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
|
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
|
||||||
<solid android:color="?android:colorAccent"/>
|
<solid android:color="?android:colorAccent"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:width="20sp"
|
||||||
|
android:height="20sp"
|
||||||
|
android:drawable="@drawable/ic_fluent_arrow_repeat_all_20_filled" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:width="20sp"
|
||||||
|
android:height="20sp"
|
||||||
|
android:drawable="@drawable/ic_fluent_arrow_reply_20_filled" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M12 4.5c-4.694 0-8.5 3.806-8.5 8.5 0 2.345 0.948 4.466 2.484 6.005 0.293 0.293 0.293 0.768 0 1.06-0.294 0.293-0.769 0.293-1.061 0C3.118 18.256 2 15.758 2 13 2 7.477 6.477 3 12 3s10 4.477 10 10c0 2.758-1.118 5.256-2.923 7.065-0.292 0.293-0.767 0.293-1.06 0-0.293-0.292-0.294-0.767-0.001-1.06C19.552 17.467 20.5 15.345 20.5 13c0-4.694-3.806-8.5-8.5-8.5zM12 8c-2.761 0-5 2.239-5 5 0 1.382 0.56 2.632 1.466 3.537 0.293 0.293 0.293 0.768 0 1.06-0.292 0.294-0.767 0.294-1.06 0.001C6.229 16.423 5.5 14.796 5.5 13c0-3.59 2.91-6.5 6.5-6.5s6.5 2.91 6.5 6.5c0 1.796-0.73 3.423-1.906 4.598-0.293 0.293-0.768 0.293-1.06 0-0.293-0.293-0.293-0.768 0-1.06C16.44 15.631 17 14.381 17 13c0-2.761-2.239-5-5-5zm0 2.5c-1.38 0-2.5 1.12-2.5 2.5s1.12 2.5 2.5 2.5 2.5-1.12 2.5-2.5-1.12-2.5-2.5-2.5zM11 13c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:width="20sp"
|
||||||
|
android:height="20sp"
|
||||||
|
android:drawable="@drawable/ic_fluent_earth_20_regular" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:width="20sp"
|
||||||
|
android:height="20sp"
|
||||||
|
android:drawable="@drawable/ic_fluent_lock_closed_20_filled" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:width="20sp"
|
||||||
|
android:height="20sp"
|
||||||
|
android:drawable="@drawable/ic_fluent_lock_open_20_regular" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:width="20sp"
|
||||||
|
android:height="20sp"
|
||||||
|
android:drawable="@drawable/ic_fluent_number_symbol_20_filled" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M3.28 2.22c-0.293-0.293-0.767-0.293-1.06 0-0.293 0.293-0.293 0.767 0 1.06l4.804 4.805-3.867 0.561C2.05 8.807 1.607 10.168 2.41 10.95l3.815 3.719-0.9 5.251c-0.19 1.103 0.968 1.944 1.958 1.423l4.716-2.479 4.716 2.48c0.99 0.52 2.148-0.32 1.96-1.424l-0.04-0.223 2.085 2.084c0.293 0.293 0.768 0.293 1.061 0 0.293-0.292 0.293-0.767 0-1.06L3.28 2.22zm13.518 15.639l0.345 2.014-4.516-2.374c-0.394-0.207-0.864-0.207-1.257 0l-4.516 2.374 0.862-5.03c0.075-0.437-0.07-0.884-0.388-1.194l-3.654-3.562 4.673-0.679 8.45 8.45zm3.525-7.772l-3.572 3.482 1.06 1.06 3.777-3.68c0.8-0.781 0.359-2.142-0.748-2.303L15.567 7.88l-2.358-4.777c-0.495-1.004-1.926-1.004-2.421 0L9.3 6.117l1.12 1.12 1.578-3.2 2.259 4.577c0.196 0.398 0.577 0.674 1.016 0.738l5.05 0.734z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<item android:drawable="@drawable/ic_fluent_person_add_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
<item android:drawable="@drawable/ic_fluent_person_add_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
||||||
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
||||||
<shape android:shape="oval">
|
<shape android:shape="oval">
|
||||||
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
|
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
|
||||||
<solid android:color="?android:colorAccent"/>
|
<solid android:color="?android:colorAccent"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<item android:drawable="@drawable/ic_fluent_settings_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
<item android:drawable="@drawable/ic_fluent_settings_24_regular" android:left="2dp" android:right="2dp" android:top="2dp" android:bottom="2dp"/>
|
||||||
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
<item android:width="14dp" android:height="14dp" android:gravity="top|right">
|
||||||
<shape android:shape="oval">
|
<shape android:shape="oval">
|
||||||
<stroke android:color="?android:colorPrimary" android:width="2dp"/>
|
<stroke android:color="?appkitToolbarBackground" android:width="2dp"/>
|
||||||
<solid android:color="?android:colorAccent"/>
|
<solid android:color="?android:colorAccent"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -1,120 +1,161 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<org.joinmastodon.android.ui.views.MaxWidthFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:orientation="horizontal"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:maxWidth="600sp"
|
||||||
|
app:defaultWidth="450sp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="48dp"
|
android:layout_height="wrap_content">
|
||||||
android:paddingHorizontal="16dp">
|
|
||||||
|
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
android:id="@+id/reply_btn"
|
android:orientation="horizontal"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:minWidth="56dp">
|
android:paddingHorizontal="11sp">
|
||||||
<TextView
|
<!-- avatar width (46sp) / 2 - button width (24sp) / 2 -->
|
||||||
android:id="@+id/reply"
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/reply_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24sp"
|
||||||
|
android:layout_height="24sp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
|
android:src="@drawable/ic_fluent_chat_multiple_24_selector_text"
|
||||||
|
android:tint="?android:textColorSecondary"
|
||||||
|
android:gravity="center_vertical" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reply"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:minWidth="16dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textAppearance="@style/m3_label_large"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="123"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/boost_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foregroundTint="@color/boost_icon"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24sp"
|
||||||
|
android:layout_height="24sp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
|
android:src="@drawable/ic_boost"
|
||||||
|
android:tint="@color/boost_icon"
|
||||||
|
android:gravity="center_vertical" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/boost"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:minWidth="16dp"
|
||||||
|
android:textColor="@color/boost_icon"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textAppearance="@style/m3_label_large"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="123"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/favorite_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24sp"
|
||||||
|
android:layout_height="24sp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
|
android:src="@drawable/ic_fluent_star_24_selector"
|
||||||
|
android:tint="@color/favorite_icon"
|
||||||
|
android:gravity="center_vertical" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/favorite"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:minWidth="16dp"
|
||||||
|
android:textColor="@color/favorite_icon"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textAppearance="@style/m3_label_large"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="123"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/bookmark_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingVertical="12dp">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/bookmark"
|
||||||
|
android:layout_width="24sp"
|
||||||
|
android:layout_height="24sp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:src="@drawable/ic_fluent_bookmark_24_selector"
|
||||||
|
android:tint="@color/bookmark_icon"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textAppearance="@style/m3_label_large" />
|
||||||
|
</FrameLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/share_btn"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center_vertical"
|
android:paddingVertical="12dp">
|
||||||
android:drawableStart="@drawable/ic_fluent_chat_multiple_24_selector_text"
|
<ImageView
|
||||||
android:drawablePadding="8dp"
|
android:id="@+id/share"
|
||||||
android:paddingHorizontal="8dp"
|
android:layout_width="24sp"
|
||||||
android:drawableTint="?android:textColorSecondary"
|
android:layout_height="24sp"
|
||||||
android:gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:textAppearance="@style/m3_label_large"
|
android:layout_marginHorizontal="16dp"
|
||||||
tools:text="123"/>
|
android:src="@drawable/ic_fluent_share_24_regular"
|
||||||
</FrameLayout>
|
android:tint="?android:textColorSecondary"
|
||||||
|
android:gravity="center_vertical"/>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<Space
|
</LinearLayout>
|
||||||
android:layout_width="0px"
|
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
|
||||||
android:layout_height="1px"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/boost_btn"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:minWidth="56dp">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/boost"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:drawableStart="@drawable/ic_boost"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:paddingHorizontal="8dp"
|
|
||||||
android:drawableTint="@color/boost_icon"
|
|
||||||
android:textColor="@color/boost_icon"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:textAppearance="@style/m3_label_large"
|
|
||||||
tools:text="123"/>
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<Space
|
|
||||||
android:layout_width="0px"
|
|
||||||
android:layout_height="1px"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/favorite_btn"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:minWidth="56dp">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/favorite"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:drawableStart="@drawable/ic_fluent_star_24_selector"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:paddingHorizontal="8dp"
|
|
||||||
android:drawableTint="@color/favorite_icon"
|
|
||||||
android:textColor="@color/favorite_icon"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:textAppearance="@style/m3_label_large"
|
|
||||||
tools:text="123"/>
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<Space
|
|
||||||
android:layout_width="0px"
|
|
||||||
android:layout_height="1px"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/bookmark_btn"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:minWidth="56dp">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/bookmark"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:drawableStart="@drawable/ic_fluent_bookmark_24_selector"
|
|
||||||
android:paddingHorizontal="8dp"
|
|
||||||
android:drawableTint="@color/bookmark_icon"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:textAppearance="@style/m3_label_large" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<Space
|
|
||||||
android:layout_width="0px"
|
|
||||||
android:layout_height="1px"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/share_btn"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/share"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:src="@drawable/ic_fluent_share_24_regular"
|
|
||||||
android:paddingHorizontal="8dp"
|
|
||||||
android:tint="?android:textColorSecondary"
|
|
||||||
android:gravity="center_vertical"/>
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -6,79 +6,79 @@
|
|||||||
android:paddingEnd="4dp"
|
android:paddingEnd="4dp"
|
||||||
android:paddingStart="16dp">
|
android:paddingStart="16dp">
|
||||||
|
|
||||||
<ImageView
|
<LinearLayout
|
||||||
android:id="@+id/more"
|
android:id="@+id/buttons"
|
||||||
android:layout_width="36dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="36dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="13dp"
|
android:layout_marginTop="13dp"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true">
|
||||||
android:background="?android:actionBarItemBackground"
|
|
||||||
android:contentDescription="@string/more_options"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_fluent_more_vertical_20_filled"
|
|
||||||
android:tint="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/delete_notification"
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_marginTop="13dp"
|
|
||||||
android:layout_toStartOf="@id/more"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:background="?android:actionBarItemBackground"
|
|
||||||
android:contentDescription="@string/sk_delete_notification"
|
|
||||||
android:tooltipText="@string/sk_delete_notification"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_fluent_dismiss_20_filled"
|
|
||||||
android:tint="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/visibility"
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_marginTop="13dp"
|
|
||||||
android:layout_toStartOf="@id/delete_notification"
|
|
||||||
android:background="?android:actionBarItemBackground"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_visibility"
|
|
||||||
android:tint="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/collapse_btn"
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_marginTop="13dp"
|
|
||||||
android:layout_toStartOf="@id/visibility"
|
|
||||||
android:background="?android:actionBarItemBackground"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:importantForAccessibility="noHideDescendants">
|
|
||||||
|
|
||||||
<!-- wrapping this button so the flip animation doesn't flip the background and the tooltip
|
|
||||||
isn't displaced by the -1 scale -->
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/collapse_btn_icon"
|
android:id="@+id/unread_indicator"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="36dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="36dp"
|
||||||
android:layout_gravity="center"
|
android:visibility="gone"
|
||||||
android:importantForAccessibility="no"
|
android:tint="?android:colorAccent"
|
||||||
android:scaleType="center"
|
android:scaleType="center"
|
||||||
android:src="@drawable/ic_fluent_chevron_down_20_filled"
|
android:src="@drawable/ic_fluent_circle_small_20_filled" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/collapse_btn"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:background="?android:actionBarItemBackground"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:importantForAccessibility="noHideDescendants">
|
||||||
|
|
||||||
|
<!-- wrapping this button so the flip animation doesn't flip the background and the tooltip
|
||||||
|
isn't displaced by the -1 scale -->
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/collapse_btn_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_fluent_chevron_down_20_filled"
|
||||||
|
android:tint="?android:textColorSecondary" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/visibility"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:background="?android:actionBarItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_visibility"
|
||||||
android:tint="?android:textColorSecondary" />
|
android:tint="?android:textColorSecondary" />
|
||||||
|
|
||||||
</FrameLayout>
|
<ImageView
|
||||||
|
android:id="@+id/delete_notification"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:background="?android:actionBarItemBackground"
|
||||||
|
android:contentDescription="@string/sk_delete_notification"
|
||||||
|
android:tooltipText="@string/sk_delete_notification"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_fluent_dismiss_20_filled"
|
||||||
|
android:tint="?android:textColorSecondary" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/more"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:background="?android:actionBarItemBackground"
|
||||||
|
android:contentDescription="@string/more_options"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_fluent_more_vertical_20_filled"
|
||||||
|
android:tint="?android:textColorSecondary" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/unread_indicator"
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:layout_marginTop="13dp"
|
|
||||||
android:layout_toStartOf="@id/collapse_btn"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:tint="?android:colorAccent"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_fluent_circle_small_20_filled" />
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/avatar"
|
android:id="@+id/avatar"
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_toStartOf="@id/unread_indicator"
|
android:layout_toStartOf="@id/buttons"
|
||||||
android:layout_toEndOf="@id/avatar"
|
android:layout_toEndOf="@id/avatar"
|
||||||
android:layout_above="@+id/username_wrap">
|
android:layout_above="@+id/username_wrap">
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_toStartOf="@id/unread_indicator"
|
android:layout_toStartOf="@id/buttons"
|
||||||
android:layout_toEndOf="@id/avatar"
|
android:layout_toEndOf="@id/avatar"
|
||||||
android:layout_alignBottom="@id/avatar"
|
android:layout_alignBottom="@id/avatar"
|
||||||
android:layoutDirection="locale"
|
android:layoutDirection="locale"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
android:paddingTop="16dp"
|
android:paddingTop="16dp"
|
||||||
android:paddingBottom="6dp"
|
android:paddingBottom="6dp"
|
||||||
android:textAppearance="@style/m3_title_small"
|
android:textAppearance="@style/m3_title_small"
|
||||||
android:drawableStart="@drawable/ic_fluent_arrow_reply_20_filled"
|
android:drawableStart="@drawable/ic_fluent_arrow_reply_20sp_filled"
|
||||||
android:drawableTint="?android:textColorSecondary"
|
android:drawableTint="?android:textColorSecondary"
|
||||||
android:drawablePadding="6dp"
|
android:drawablePadding="6dp"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:layout_marginEnd="40dp">
|
android:layout_marginEnd="40dp"
|
||||||
|
android:minWidth="40dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
12
mastodon/src/main/res/menu/settings_auto_reveal_spoiler.xml
Normal file
12
mastodon/src/main/res/menu/settings_auto_reveal_spoiler.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/auto_reveal_never"
|
||||||
|
android:title="@string/sk_settings_auto_reveal_nobody" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/auto_reveal_threads"
|
||||||
|
android:title="@string/sk_settings_auto_reveal_author" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/auto_reveal_discussions"
|
||||||
|
android:title="@string/sk_settings_auto_reveal_anyone" />
|
||||||
|
</menu>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@+id/prefix_replies_never" android:title="@string/sk_settings_prefix_replies_never" />
|
||||||
|
<item android:id="@+id/prefix_replies_to_others" android:title="@string/sk_settings_prefix_replies_to_others" />
|
||||||
|
<item android:id="@+id/prefix_replies_always" android:title="@string/sk_settings_prefix_replies_always" />
|
||||||
|
</menu>
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="sk_confirm_delete_and_redraft">هل أنت متأكد أنك تريد حذف وإعادة صياغة هذا المنشور؟</string>
|
|
||||||
<string name="sk_visibility_unlisted">غير مدرج</string>
|
|
||||||
<string name="sk_list_timelines">القوائم</string>
|
|
||||||
<string name="sk_follow_requests">طلبات المتابعة</string>
|
|
||||||
<string name="sk_pinned_posts">مدبّس</string>
|
|
||||||
<string name="sk_delete_and_redraft">حذف وإعادة الصياغة</string>
|
|
||||||
<string name="sk_confirm_delete_and_redraft_title">حذف وإعادة صياغة الرسالة</string>
|
|
||||||
<string name="sk_pin_post">تدبيس على الصفحة الشخصية</string>
|
|
||||||
<string name="sk_confirm_pin_post_title">تدبيس الرسالة على الصفحة الشخصية</string>
|
|
||||||
<string name="sk_settings_show_federated_timeline">إظهار الخيط الفديرالي</string>
|
|
||||||
<string name="sk_settings_contribute">المساهمة في Megalodon</string>
|
|
||||||
<string name="sk_accept_follow_request">قبول طلب المتابعة</string>
|
|
||||||
<string name="sk_reject_follow_request">رفض طلب المتابعة</string>
|
|
||||||
<string name="sk_lists_with_user">قوائم بها %s</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
</resources>
|
|
||||||
@@ -2,7 +2,16 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="sk_confirm_delete_and_redraft">هل أنت متأكد أنك تريد حذف وإعادة صياغة هذا المنشور؟</string>
|
<string name="sk_confirm_delete_and_redraft">هل أنت متأكد أنك تريد حذف وإعادة صياغة هذا المنشور؟</string>
|
||||||
<string name="sk_pin_post">تثبيت في الملف الشخصي</string>
|
<string name="sk_pin_post">تثبيت في الملف الشخصي</string>
|
||||||
<string name="sk_pinned_posts">مثبت</string>
|
<string name="sk_pinned_posts">المُثَبَّتَة</string>
|
||||||
<string name="sk_delete_and_redraft">حذف وإعادة صياغة</string>
|
<string name="sk_delete_and_redraft">حذف وإعادة صياغة</string>
|
||||||
<string name="sk_confirm_delete_and_redraft_title">حذف وإعادة صياغة المنشور</string>
|
<string name="sk_confirm_delete_and_redraft_title">حذف وإعادة صياغة المنشور</string>
|
||||||
|
<string name="sk_visibility_unlisted">غير مدرج</string>
|
||||||
|
<string name="sk_list_timelines">القوائم</string>
|
||||||
|
<string name="sk_follow_requests">طلبات المتابعة</string>
|
||||||
|
<string name="sk_confirm_pin_post_title">تدبيس الرسالة على الصفحة الشخصية</string>
|
||||||
|
<string name="sk_settings_show_federated_timeline">إظهار الخيط الفديرالي</string>
|
||||||
|
<string name="sk_settings_contribute">المساهمة في Megalodon</string>
|
||||||
|
<string name="sk_accept_follow_request">قبول طلب المتابعة</string>
|
||||||
|
<string name="sk_reject_follow_request">رفض طلب المتابعة</string>
|
||||||
|
<string name="sk_lists_with_user">قوائم بها %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -20,11 +20,20 @@
|
|||||||
<string name="share_toot_title">শেয়ার করুন</string>
|
<string name="share_toot_title">শেয়ার করুন</string>
|
||||||
<string name="settings">সেটিংস</string>
|
<string name="settings">সেটিংস</string>
|
||||||
<string name="cancel">বাতিল করুন</string>
|
<string name="cancel">বাতিল করুন</string>
|
||||||
|
<plurals name="followers">
|
||||||
|
<item quantity="one">জন ফলোয়ার</item>
|
||||||
|
<item quantity="other">জন ফলোয়ারস</item>
|
||||||
|
</plurals>
|
||||||
<plurals name="posts">
|
<plurals name="posts">
|
||||||
<item quantity="one">পোস্ট</item>
|
<item quantity="one">পোস্ট</item>
|
||||||
<item quantity="other">পোস্টগুলো</item>
|
<item quantity="other">পোস্টগুলো</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="posts">পোস্টগুলো</string>
|
<string name="posts">পোস্টগুলো</string>
|
||||||
|
<string name="media">মিডিয়া</string>
|
||||||
|
<string name="button_follow">ফলো করুন</string>
|
||||||
|
<string name="button_following">ফলো করছেন</string>
|
||||||
|
<string name="edit_profile">প্রোফাইল সংশোধন করুন</string>
|
||||||
|
<string name="mention_user">%s -কে পিং করুন</string>
|
||||||
<string name="share_user">%s -কে শেয়ার করুন</string>
|
<string name="share_user">%s -কে শেয়ার করুন</string>
|
||||||
<string name="mute_user">%s -কে মিউট করুন</string>
|
<string name="mute_user">%s -কে মিউট করুন</string>
|
||||||
<string name="unmute_user">%s -কে আনমিউট করুন</string>
|
<string name="unmute_user">%s -কে আনমিউট করুন</string>
|
||||||
@@ -53,6 +62,22 @@
|
|||||||
<item quantity="one">%d দিন</item>
|
<item quantity="one">%d দিন</item>
|
||||||
<item quantity="other">%d দিন</item>
|
<item quantity="other">%d দিন</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="x_seconds_left">
|
||||||
|
<item quantity="one">%d সেকেন্ড বাকি</item>
|
||||||
|
<item quantity="other">%d সেকেন্ড বাকি</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="x_minutes_left">
|
||||||
|
<item quantity="one">%d মিনিট বাকি</item>
|
||||||
|
<item quantity="other">%d মিনিট বাকি</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="x_hours_left">
|
||||||
|
<item quantity="one">%d ঘণ্টা বাকি</item>
|
||||||
|
<item quantity="other">%d ঘণ্টা বাকি</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="x_days_left">
|
||||||
|
<item quantity="one">%d দিন বাকি</item>
|
||||||
|
<item quantity="other">%d দিন বাকি</item>
|
||||||
|
</plurals>
|
||||||
<string name="poll_closed">বন্ধ</string>
|
<string name="poll_closed">বন্ধ</string>
|
||||||
<string name="confirm_mute_title">অ্যাকাউন্টটি মিউট করুন</string>
|
<string name="confirm_mute_title">অ্যাকাউন্টটি মিউট করুন</string>
|
||||||
<string name="do_mute">মিউট করুন</string>
|
<string name="do_mute">মিউট করুন</string>
|
||||||
@@ -89,6 +114,17 @@
|
|||||||
<item quantity="other">%d jon ব্যক্তিরা বলছেন</item>
|
<item quantity="other">%d jon ব্যক্তিরা বলছেন</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="sending_report">রিপোর্ট পাঠানো হচ্ছে…</string>
|
<string name="sending_report">রিপোর্ট পাঠানো হচ্ছে…</string>
|
||||||
|
<string name="report_sent_title">রিপোর্ট করার জন্য আপনাকে ধন্যবাদ, আমরা এটি শীঘ্রই দেখব.</string>
|
||||||
|
<string name="report_sent_subtitle">আমরা যতক্ষণে আপনার রিপোর্ট পুনর্বিবেচনা করছি, আপনি %s এর বিরুদ্ধে ব্যবস্থা নিতে পারেন.</string>
|
||||||
|
<string name="back">ফিরে যান</string>
|
||||||
|
<string name="search_communities">সার্ভারের নাম বা লিঙ্ক</string>
|
||||||
|
<string name="instance_rules_title">সার্ভারের নিয়মাবলী</string>
|
||||||
|
<string name="signup_title">অ্যাকাউন্ট তৈরি করুন</string>
|
||||||
|
<string name="display_name">নাম</string>
|
||||||
|
<string name="username">ইউজারনেম</string>
|
||||||
|
<string name="email">ই-মেইল</string>
|
||||||
|
<string name="password">পাসওয়ার্ড</string>
|
||||||
|
<string name="confirm_password">পাসওয়ার্ড নিশ্চিত করুন</string>
|
||||||
<!-- %s is the email address -->
|
<!-- %s is the email address -->
|
||||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||||
<!-- %s is version like 1.2.3 -->
|
<!-- %s is version like 1.2.3 -->
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
<string name="report_sent_subtitle">Während wir den Vorfall überprüfen, kannst du gegen %s weitere Maßnahmen ergreifen.</string>
|
<string name="report_sent_subtitle">Während wir den Vorfall überprüfen, kannst du gegen %s weitere Maßnahmen ergreifen.</string>
|
||||||
<string name="unfollow_user">%s entfolgen</string>
|
<string name="unfollow_user">%s entfolgen</string>
|
||||||
<string name="unfollow">Entfolgen</string>
|
<string name="unfollow">Entfolgen</string>
|
||||||
<string name="mute_user_explain">Du wirst die eigenen und geteilten Beiträge des Kontos nicht mehr sehen können. Dass du das Profil stummgeschaltet hast, erfährt die Person nicht.</string>
|
<string name="mute_user_explain">Du wirst deren (geteilte) Beiträge auf deiner Startseite nicht mehr sehen können. Sie werden nicht erfahren, dass sie stummgeschaltet sind.</string>
|
||||||
<string name="block_user_explain">Dir wird es nicht länger möglich sein, die Beiträge dieses Konto zu sehen. Das blockierte Profil wird nicht mehr in der Lage sein, deine Beiträge zu sehen oder dir zu folgen. Die Person hinter dem Konto wird mitbekommen, dass du ihr Konto gesperrt hast.</string>
|
<string name="block_user_explain">Dir wird es nicht länger möglich sein, die Beiträge dieses Konto zu sehen. Das blockierte Profil wird nicht mehr in der Lage sein, deine Beiträge zu sehen oder dir zu folgen. Die Person hinter dem Konto wird mitbekommen, dass du ihr Konto gesperrt hast.</string>
|
||||||
<string name="report_personal_title">Möchtest du das nicht mehr sehen?</string>
|
<string name="report_personal_title">Möchtest du das nicht mehr sehen?</string>
|
||||||
<string name="report_personal_subtitle">Wenn du etwas auf Mastodon siehst, das dir nicht gefällt, kannst du die Person aus deinem Umfeld entfernen.</string>
|
<string name="report_personal_subtitle">Wenn du etwas auf Mastodon siehst, das dir nicht gefällt, kannst du die Person aus deinem Umfeld entfernen.</string>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
<string name="instance_catalog_subtitle">Wähle einen Server basierend auf deinen Interessen oder deiner Region – oder einfach einen allgemeinen. Du kannst trotzdem mit jedem interagieren, egal auf welchem Server.</string>
|
<string name="instance_catalog_subtitle">Wähle einen Server basierend auf deinen Interessen oder deiner Region – oder einfach einen allgemeinen. Du kannst trotzdem mit jedem interagieren, egal auf welchem Server.</string>
|
||||||
<string name="search_communities">Servername oder -adresse</string>
|
<string name="search_communities">Servername oder -adresse</string>
|
||||||
<string name="instance_rules_title">Server-Regeln</string>
|
<string name="instance_rules_title">Server-Regeln</string>
|
||||||
<string name="instance_rules_subtitle">Mit dem Fortfahren erklärst du dich damit einverstanden, die folgenden Regeln zu befolgen, die von den %s-Moderatoren aufgestellt und umgesetzt werden.</string>
|
<string name="instance_rules_subtitle">Solltest du fortfahren, erklärst du dich mit den Serverregeln, die die Moderator*innen von %s aufgestellt haben und durchsetzen werden, einverstanden.</string>
|
||||||
<string name="signup_title">Konto erstellen</string>
|
<string name="signup_title">Konto erstellen</string>
|
||||||
<string name="edit_photo">bearbeiten</string>
|
<string name="edit_photo">bearbeiten</string>
|
||||||
<string name="display_name">Name</string>
|
<string name="display_name">Name</string>
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
<string name="confirm_email_title">Überprüfe deinen Posteingang</string>
|
<string name="confirm_email_title">Überprüfe deinen Posteingang</string>
|
||||||
<!-- %s is the email address -->
|
<!-- %s is the email address -->
|
||||||
<string name="confirm_email_subtitle">Klicke auf den Link, den wir dir geschickt haben, um %s zu bestätigen. Wir warten hier auf dich.</string>
|
<string name="confirm_email_subtitle">Klicke auf den Link, den wir dir geschickt haben, um %s zu bestätigen. Wir warten hier auf dich.</string>
|
||||||
<string name="confirm_email_didnt_get">Kein Link erhalten?</string>
|
<string name="confirm_email_didnt_get">Keinen Link erhalten?</string>
|
||||||
<string name="resend">Erneut abschicken</string>
|
<string name="resend">Erneut abschicken</string>
|
||||||
<string name="open_email_app">E-Mail-App öffnen</string>
|
<string name="open_email_app">E-Mail-App öffnen</string>
|
||||||
<string name="resent_email">Bestätigung per E-Mail zugeschickt</string>
|
<string name="resent_email">Bestätigung per E-Mail zugeschickt</string>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<string name="sk_already_reblogged">Bereits geteilt</string>
|
<string name="sk_already_reblogged">Bereits geteilt</string>
|
||||||
<string name="sk_reply_as">Antworten mit anderem Konto</string>
|
<string name="sk_reply_as">Antworten mit anderem Konto</string>
|
||||||
<string name="sk_settings_uniform_icon_for_notifications">Einheitliches Icon für alle Benachrichtigungen</string>
|
<string name="sk_settings_uniform_icon_for_notifications">Einheitliches Icon für alle Benachrichtigungen</string>
|
||||||
<string name="sk_forward_report_to">Weiterleiten zu %s</string>
|
<string name="sk_forward_report_to">Weiterleiten an %s</string>
|
||||||
<string name="sk_unsent_posts">Nicht gesendete Beiträge</string>
|
<string name="sk_unsent_posts">Nicht gesendete Beiträge</string>
|
||||||
<string name="sk_draft">Entwurf</string>
|
<string name="sk_draft">Entwurf</string>
|
||||||
<string name="sk_schedule">Planen</string>
|
<string name="sk_schedule">Planen</string>
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
<string name="sk_settings_see_new_posts_button">“Neue Beiträge anzeigen”-Button</string>
|
<string name="sk_settings_see_new_posts_button">“Neue Beiträge anzeigen”-Button</string>
|
||||||
<string name="sk_settings_server_version">Server-Version: %s</string>
|
<string name="sk_settings_server_version">Server-Version: %s</string>
|
||||||
<string name="sk_notify_poll_results">Umfrage-Ergebnisse</string>
|
<string name="sk_notify_poll_results">Umfrage-Ergebnisse</string>
|
||||||
<string name="sk_settings_prefix_reply_cw_with_re">CWs beim Antworten “re:” voranstellen</string>
|
<string name="sk_settings_prefix_reply_cw_with_re">Voranstellen von „re:“ an CWs in Antworten an</string>
|
||||||
<string name="sk_filtered">Gefiltert: %s</string>
|
<string name="sk_filtered">Gefiltert: %s</string>
|
||||||
<string name="sk_expand">Erweitern</string>
|
<string name="sk_expand">Erweitern</string>
|
||||||
<string name="sk_collapse">Einklappen</string>
|
<string name="sk_collapse">Einklappen</string>
|
||||||
@@ -274,4 +274,34 @@
|
|||||||
<string name="sk_settings_confirm_before_reblog">Vor dem Teilen bestätigen</string>
|
<string name="sk_settings_confirm_before_reblog">Vor dem Teilen bestätigen</string>
|
||||||
<string name="sk_reacted">hat reagiert</string>
|
<string name="sk_reacted">hat reagiert</string>
|
||||||
<string name="sk_reacted_with">hat mit %s reagiert</string>
|
<string name="sk_reacted_with">hat mit %s reagiert</string>
|
||||||
|
<string name="sk_external_share_title">Mit Konto teilen</string>
|
||||||
|
<string name="sk_external_share_or_open_title">Mit Konto teilen oder öffnen</string>
|
||||||
|
<string name="sk_timeline_bubble">Bubble</string>
|
||||||
|
<string name="sk_settings_default_content_type_explanation">Vorausgewählter Inhaltstyp für neue Beiträge – überschreibt den Wert, der unter „Einstellungen für Beiträge“ gesetzt ist.</string>
|
||||||
|
<string name="sk_open_in_app_failed">Konnte nicht in der App öffnen</string>
|
||||||
|
<string name="sk_content_type_unspecified">Nicht angegeben</string>
|
||||||
|
<string name="sk_content_type_plain">Nur Text</string>
|
||||||
|
<string name="sk_content_type_html">HTML</string>
|
||||||
|
<string name="sk_content_type_markdown">Markdown</string>
|
||||||
|
<string name="sk_content_type_bbcode">BBCode</string>
|
||||||
|
<string name="sk_content_type_mfm">MFM</string>
|
||||||
|
<string name="sk_content_type">Inhaltstyp</string>
|
||||||
|
<string name="sk_bubble_timeline_info_banner">Das sind die neuesten Beiträge aus dem Netzwerk, das deine Instanz-Admins kuratiert haben.</string>
|
||||||
|
<string name="sk_settings_content_types">Formatierung aktivieren</string>
|
||||||
|
<string name="sk_settings_default_content_type">Standard-Inhaltstyp</string>
|
||||||
|
<string name="sk_instance_info_unavailable">Informationen zur Instanz momentan nicht verfügbar</string>
|
||||||
|
<string name="sk_open_in_app">In App öffnen</string>
|
||||||
|
<string name="sk_settings_content_types_explanation">Dadurch lässt beim Erstellen von Beiträgen ein Inhaltstyp wie Markdown angeben. Nicht alle Instanzen unterstützen das.</string>
|
||||||
|
<string name="sk_settings_allow_remote_loading">Infos von Remote-Instanzen laden</string>
|
||||||
|
<string name="sk_no_remote_info_hint">keine Remote-Infos abrufbar</string>
|
||||||
|
<string name="sk_error_loading_profile">Konnte das Profil via %s nicht laden</string>
|
||||||
|
<string name="sk_settings_allow_remote_loading_explanation">Für vollständigere Auflistung von Follower*innen, Likes und Boosts können die Informationen von der Ursprungs-Instanz geladen werden.</string>
|
||||||
|
<string name="sk_settings_auto_reveal_equal_spoilers">Zeigen von gleichen CWs in Antworten von</string>
|
||||||
|
<string name="sk_settings_auto_reveal_nobody">niemandem</string>
|
||||||
|
<string name="sk_settings_auto_reveal_author">Autor*in</string>
|
||||||
|
<string name="sk_settings_auto_reveal_anyone">allen</string>
|
||||||
|
<string name="sk_settings_prefix_replies_to_others">andere</string>
|
||||||
|
<string name="sk_settings_prefix_replies_always">alle</string>
|
||||||
|
<string name="sk_settings_prefix_replies_never">niemanden</string>
|
||||||
|
<string name="sk_settings_forward_report_default">Standardwert „Meldung weiterleiten“-Schalter</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -245,9 +245,9 @@
|
|||||||
<string name="sk_settings_glitch_instance">Glitch modo sólo local</string>
|
<string name="sk_settings_glitch_instance">Glitch modo sólo local</string>
|
||||||
<string name="sk_settings_glitch_mode_explanation">Habilita esta opción si tu instancia local funciona con Glitch. No es necesario para Hometown o Akkoma.</string>
|
<string name="sk_settings_glitch_mode_explanation">Habilita esta opción si tu instancia local funciona con Glitch. No es necesario para Hometown o Akkoma.</string>
|
||||||
<string name="sk_signed_up">registrado</string>
|
<string name="sk_signed_up">registrado</string>
|
||||||
<string name="sk_reported">reportado</string>
|
<string name="sk_reported">denunciado</string>
|
||||||
<string name="sk_sign_ups">Registro de usuarios</string>
|
<string name="sk_sign_ups">Registro de usuarios</string>
|
||||||
<string name="sk_new_reports">Nuevos informes</string>
|
<string name="sk_new_reports">Nuevas denuncias</string>
|
||||||
<string name="sk_settings_server_version">Versión de servidor: %s</string>
|
<string name="sk_settings_server_version">Versión de servidor: %s</string>
|
||||||
<string name="sk_notify_poll_results">Resultado de encuestas</string>
|
<string name="sk_notify_poll_results">Resultado de encuestas</string>
|
||||||
<string name="sk_filtered">Filtrado: %s</string>
|
<string name="sk_filtered">Filtrado: %s</string>
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
<string name="sk_settings_collapse_long_posts">Minimizar publicaciones largas</string>
|
<string name="sk_settings_collapse_long_posts">Minimizar publicaciones largas</string>
|
||||||
<string name="sk_unfinished_attachments">¿Corregir adjuntos\?</string>
|
<string name="sk_unfinished_attachments">¿Corregir adjuntos\?</string>
|
||||||
<string name="sk_unfinished_attachments_message">Algunos adjuntos no han terminado de subirse.</string>
|
<string name="sk_unfinished_attachments_message">Algunos adjuntos no han terminado de subirse.</string>
|
||||||
<string name="sk_settings_prefix_reply_cw_with_re">Añadir \"re:\" a respuestas a Advertencias de Contenido</string>
|
<string name="sk_settings_prefix_reply_cw_with_re">Añadir \"re:\" a Advertencias de Contenido para</string>
|
||||||
<string name="sk_spectator_mode">Modo espectador</string>
|
<string name="sk_spectator_mode">Modo espectador</string>
|
||||||
<string name="sk_settings_hide_interaction">Ocultar los botones de interacción</string>
|
<string name="sk_settings_hide_interaction">Ocultar los botones de interacción</string>
|
||||||
<string name="sk_follow_as">Seguir desde otra cuenta</string>
|
<string name="sk_follow_as">Seguir desde otra cuenta</string>
|
||||||
@@ -286,4 +286,23 @@
|
|||||||
<string name="sk_settings_default_content_type">Contenido por defecto</string>
|
<string name="sk_settings_default_content_type">Contenido por defecto</string>
|
||||||
<string name="sk_settings_content_types_explanation">Permite establecer un tipo de contenido como Markdown al crear una entrada. Ten en cuenta que no todas las instancias lo admiten.</string>
|
<string name="sk_settings_content_types_explanation">Permite establecer un tipo de contenido como Markdown al crear una entrada. Ten en cuenta que no todas las instancias lo admiten.</string>
|
||||||
<string name="sk_settings_default_content_type_explanation">Permite preseleccionar un tipo de contenido al crear nuevas entradas, anulando el valor establecido en \"Preferencias de publicación\".</string>
|
<string name="sk_settings_default_content_type_explanation">Permite preseleccionar un tipo de contenido al crear nuevas entradas, anulando el valor establecido en \"Preferencias de publicación\".</string>
|
||||||
|
<string name="sk_bubble_timeline_info_banner">Estas son las publicaciones más recientes de la red seleccionadas por los administradores de tu instancia.</string>
|
||||||
|
<string name="sk_timeline_bubble">Burbuja</string>
|
||||||
|
<string name="sk_instance_info_unavailable">Información de la instancia temporalmente no disponible</string>
|
||||||
|
<string name="sk_external_share_or_open_title">Compartir o abrir con una cuenta</string>
|
||||||
|
<string name="sk_open_in_app">Abrir en la app</string>
|
||||||
|
<string name="sk_external_share_title">Compartir con una cuenta</string>
|
||||||
|
<string name="sk_settings_auto_reveal_equal_spoilers">Mostrar CW iguales en las respuestas de</string>
|
||||||
|
<string name="sk_settings_auto_reveal_nobody">nadie</string>
|
||||||
|
<string name="sk_settings_auto_reveal_author">autor</string>
|
||||||
|
<string name="sk_settings_auto_reveal_anyone">todos</string>
|
||||||
|
<string name="sk_open_in_app_failed">No se pudo abrir en la aplicación</string>
|
||||||
|
<string name="sk_no_remote_info_hint">no hay información remota disponible</string>
|
||||||
|
<string name="sk_error_loading_profile">No se pudo cargar el perfil a través de %s</string>
|
||||||
|
<string name="sk_settings_allow_remote_loading">Cargar la información desde las instancias remotas</string>
|
||||||
|
<string name="sk_settings_allow_remote_loading_explanation">Intenta obtener listas más precisas de seguidores, Me gusta y promociones cargando la información desde la instancia de origen.</string>
|
||||||
|
<string name="sk_settings_prefix_replies_always">Todas</string>
|
||||||
|
<string name="sk_settings_prefix_replies_never">Ninguna</string>
|
||||||
|
<string name="sk_settings_prefix_replies_to_others">A otros</string>
|
||||||
|
<string name="sk_settings_forward_report_default">\"Reenviar denuncia\" activado por defecto</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -12,51 +12,85 @@
|
|||||||
<string name="in_reply_to">در پاسخ به %s</string>
|
<string name="in_reply_to">در پاسخ به %s</string>
|
||||||
<string name="notifications">اعلانها</string>
|
<string name="notifications">اعلانها</string>
|
||||||
<string name="user_followed_you">شما را دنبال میکند</string>
|
<string name="user_followed_you">شما را دنبال میکند</string>
|
||||||
<string name="user_sent_follow_request">یک درخواست دنبال کردن برای شما ارسال کرد</string>
|
<string name="user_sent_follow_request">یک درخواست پیگیری برای شما ارسال کرد</string>
|
||||||
<string name="user_favorited"> فرستهتان را پسندید</string>
|
<string name="user_favorited"> فرستهتان را پسندید</string>
|
||||||
|
<string name="notification_boosted">فرستهٔ شما را تقویت کرد</string>
|
||||||
<string name="poll_ended">نظرسنجی به پایان رسید</string>
|
<string name="poll_ended">نظرسنجی به پایان رسید</string>
|
||||||
<string name="time_seconds">%dثانیه</string>
|
<string name="time_seconds">%dثانیه</string>
|
||||||
<string name="time_minutes">%dدقیقه</string>
|
<string name="time_minutes">%dدقیقه</string>
|
||||||
<string name="time_hours">%dساعت</string>
|
<string name="time_hours">%dساعت</string>
|
||||||
<string name="time_days">%dروز</string>
|
<string name="time_days">%dروز</string>
|
||||||
<string name="share_toot_title">اشتراکگذاری</string>
|
<string name="share_toot_title">همرسانی</string>
|
||||||
<string name="settings">تنظیمات</string>
|
<string name="settings">تنظیمات</string>
|
||||||
<string name="publish">انتشار</string>
|
<string name="publish">انتشار</string>
|
||||||
<string name="discard_draft">پیشنویس کنار گذاشته شود؟</string>
|
<string name="discard_draft">پیشنویس کنار گذاشته شود؟</string>
|
||||||
<string name="discard">صرفنظر کردن</string>
|
<string name="discard">صرفنظر کردن</string>
|
||||||
<string name="cancel">لغو</string>
|
<string name="cancel">لغو</string>
|
||||||
<string name="posts">پستها</string>
|
<plurals name="followers">
|
||||||
<string name="posts_and_replies">پستها و پاسخها</string>
|
<item quantity="one">پیگیرنده</item>
|
||||||
|
<item quantity="other">پیگیرندگان</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="posts">
|
||||||
|
<item quantity="one">فرسته</item>
|
||||||
|
<item quantity="other">فرستهها</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="posts">فرستهها</string>
|
||||||
|
<string name="posts_and_replies">فرستهها و پاسخها</string>
|
||||||
<string name="media">رسانه</string>
|
<string name="media">رسانه</string>
|
||||||
<string name="profile_about">درباره</string>
|
<string name="profile_about">درباره</string>
|
||||||
<string name="button_follow">فالو</string>
|
<string name="button_follow">پیگیری</string>
|
||||||
|
<string name="edit_profile">ویرایش نمایه</string>
|
||||||
<string name="share_user">اشتراکگذاری %s</string>
|
<string name="share_user">اشتراکگذاری %s</string>
|
||||||
<string name="mute_user">بیصدا %s</string>
|
<string name="mute_user">بیصدا %s</string>
|
||||||
|
<string name="unmute_user">ناخموشی %s</string>
|
||||||
<string name="block_user">مسدود %s</string>
|
<string name="block_user">مسدود %s</string>
|
||||||
<string name="unblock_user">رفع مسدودیت %s</string>
|
<string name="unblock_user">رفع مسدودیت %s</string>
|
||||||
|
<string name="report_user">گزارش کردن %s</string>
|
||||||
<string name="profile_joined">عضو شد</string>
|
<string name="profile_joined">عضو شد</string>
|
||||||
<string name="done">انجام شد</string>
|
<string name="done">انجام شد</string>
|
||||||
<string name="loading">درحال بارگذاری…</string>
|
<string name="loading">درحال بارگذاری…</string>
|
||||||
<string name="field_label">برچسب</string>
|
<string name="field_label">برچسب</string>
|
||||||
<string name="field_content">محتوا</string>
|
<string name="field_content">محتوا</string>
|
||||||
<string name="saving">درحال ذخیرهسازی…</string>
|
<string name="saving">درحال ذخیرهسازی…</string>
|
||||||
|
<string name="poll_closed">پایانیافته</string>
|
||||||
|
<string name="confirm_mute_title">خموشی حساب</string>
|
||||||
<string name="do_mute">بیصدا</string>
|
<string name="do_mute">بیصدا</string>
|
||||||
|
<string name="confirm_unmute_title">لغو خموشی حساب</string>
|
||||||
|
<string name="do_unmute">ناخموشی</string>
|
||||||
|
<string name="confirm_block_title">مسدود کردن حساب</string>
|
||||||
|
<string name="confirm_block_domain_title">مسدود کردن دامنهٔ</string>
|
||||||
<string name="do_block">مسدود</string>
|
<string name="do_block">مسدود</string>
|
||||||
|
<string name="confirm_unblock_title">رفع مسدودی حساب</string>
|
||||||
|
<string name="confirm_unblock_domain_title">رفع مسدودیت دامنهٔ</string>
|
||||||
|
<string name="do_unblock">رفع مسدودیت</string>
|
||||||
|
<string name="button_muted">خموش</string>
|
||||||
|
<string name="button_blocked">مسدود شده</string>
|
||||||
<string name="action_vote">رأی</string>
|
<string name="action_vote">رأی</string>
|
||||||
<string name="delete">حذف</string>
|
<string name="delete">حذف</string>
|
||||||
<string name="confirm_delete_title">حذف پست</string>
|
<string name="confirm_delete_title">حذف فرسته</string>
|
||||||
<string name="play">پخش</string>
|
<string name="play">پخش</string>
|
||||||
<string name="pause">توقف</string>
|
<string name="pause">توقف</string>
|
||||||
|
<string name="log_out">خروج</string>
|
||||||
|
<string name="add_account">افزودن حساب</string>
|
||||||
<string name="search_hint">جستجو</string>
|
<string name="search_hint">جستجو</string>
|
||||||
<string name="hashtags">هشتگها</string>
|
<string name="hashtags">برچسبها</string>
|
||||||
<string name="news">اخبار</string>
|
<string name="news">اخبار</string>
|
||||||
<string name="for_you">برای شما</string>
|
<string name="for_you">برای شما</string>
|
||||||
<string name="all_notifications">همه</string>
|
<string name="all_notifications">همه</string>
|
||||||
<string name="report_title">گزارش کردن %s</string>
|
<string name="report_title">گزارش کردن %s</string>
|
||||||
<string name="report_reason_personal">من این را دوست ندارم</string>
|
<string name="report_reason_personal">من این را دوست ندارم</string>
|
||||||
<string name="report_reason_spam">این هرزنامه است</string>
|
<string name="report_reason_spam">این هرزنامه است</string>
|
||||||
|
<string name="sending_report">درحال ارسال گزارش…</string>
|
||||||
|
<string name="unfollow">پینگرفتن</string>
|
||||||
<string name="back">بازگشت</string>
|
<string name="back">بازگشت</string>
|
||||||
|
<string name="instance_rules_title">قوانین کارساز</string>
|
||||||
|
<string name="signup_title">ایجاد حساب</string>
|
||||||
<string name="edit_photo">ویرایش</string>
|
<string name="edit_photo">ویرایش</string>
|
||||||
|
<string name="display_name">نام</string>
|
||||||
|
<string name="username">نام کاربری</string>
|
||||||
|
<string name="email">رایانامه</string>
|
||||||
|
<string name="password">گذرواژه</string>
|
||||||
|
<string name="confirm_password">تأیید گذرواژه</string>
|
||||||
<string name="category_activism">فعالیت</string>
|
<string name="category_activism">فعالیت</string>
|
||||||
<string name="category_all">همه</string>
|
<string name="category_all">همه</string>
|
||||||
<string name="category_art">هنر</string>
|
<string name="category_art">هنر</string>
|
||||||
@@ -68,34 +102,118 @@
|
|||||||
<string name="category_tech">فناوری</string>
|
<string name="category_tech">فناوری</string>
|
||||||
<!-- %s is the email address -->
|
<!-- %s is the email address -->
|
||||||
<string name="resend">ارسال دوباره</string>
|
<string name="resend">ارسال دوباره</string>
|
||||||
|
<string name="retry_upload">بارگذاری مجدد</string>
|
||||||
|
<string name="edit_image">ویرایش تصویر</string>
|
||||||
<string name="save">ذخیره</string>
|
<string name="save">ذخیره</string>
|
||||||
<string name="visibility_public">عمومی</string>
|
<string name="visibility_public">عمومی</string>
|
||||||
|
<string name="visibility_followers_only">فقط پیگیرندگان</string>
|
||||||
<string name="search_all">همه</string>
|
<string name="search_all">همه</string>
|
||||||
<string name="search_people">افراد</string>
|
<string name="search_people">افراد</string>
|
||||||
<string name="skip">بعدی</string>
|
<string name="skip">بعدی</string>
|
||||||
<string name="notification_type_favorite">علاقهمندیها</string>
|
<string name="notification_type_favorite">علاقهمندیها</string>
|
||||||
|
<string name="notification_type_reblog">تقویتها</string>
|
||||||
<string name="notification_type_poll">نظرسنجیها</string>
|
<string name="notification_type_poll">نظرسنجیها</string>
|
||||||
<string name="choose_account">انتخاب حساب</string>
|
<string name="choose_account">انتخاب حساب</string>
|
||||||
<string name="theme_auto">خودکار</string>
|
<string name="theme_auto">خودکار</string>
|
||||||
<string name="theme_light">روشن</string>
|
<string name="theme_light">روشن</string>
|
||||||
<string name="theme_dark">تاریک</string>
|
<string name="theme_dark">تاریک</string>
|
||||||
<string name="new_post">پست جدید</string>
|
<string name="new_post">فرسته جدید</string>
|
||||||
<string name="button_reply">پاسخ</string>
|
<string name="button_reply">پاسخ</string>
|
||||||
|
<string name="button_reblog">تقویت</string>
|
||||||
<string name="button_favorite">برگزیده</string>
|
<string name="button_favorite">برگزیده</string>
|
||||||
<string name="button_share">اشتراکگذاری</string>
|
<string name="button_share">اشتراکگذاری</string>
|
||||||
|
<string name="add_media">افزودن رسانه</string>
|
||||||
|
<string name="add_poll">افزودن نظرسنجی</string>
|
||||||
<string name="emoji">ایموجی</string>
|
<string name="emoji">ایموجی</string>
|
||||||
|
<string name="my_profile">نمایه من</string>
|
||||||
|
<string name="follow_user">پیگیری %s</string>
|
||||||
|
<string name="unfollowed_user">پینگرفتن %s</string>
|
||||||
|
<string name="open_in_browser">بازکردن در مرورگر</string>
|
||||||
<string name="clear">پاککردن</string>
|
<string name="clear">پاککردن</string>
|
||||||
<string name="download">دانلود</string>
|
<string name="download">بارگیری</string>
|
||||||
|
<string name="open_settings">باز کردن تنظیمات</string>
|
||||||
|
<string name="file_saved">پرونده ذخیره شد</string>
|
||||||
|
<string name="downloading">درحال بارگیری…</string>
|
||||||
|
<string name="local_timeline">اجتماع</string>
|
||||||
|
<string name="follows_you">پیگیرتان است</string>
|
||||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||||
|
<plurals name="x_favorites">
|
||||||
|
<item quantity="one">%,d پسندیده</item>
|
||||||
|
<item quantity="other">%,d پسندیدهها</item>
|
||||||
|
</plurals>
|
||||||
<string name="time_now">اکنون</string>
|
<string name="time_now">اکنون</string>
|
||||||
|
<string name="post_info_reblogs">تقویتها</string>
|
||||||
|
<string name="post_info_favorites">علاقهمندیها</string>
|
||||||
|
<string name="last_edit_at_x">آخرین ویرایش %s</string>
|
||||||
<string name="time_just_now">همين الان</string>
|
<string name="time_just_now">همين الان</string>
|
||||||
|
<plurals name="x_seconds_ago">
|
||||||
|
<item quantity="one">%d ثانیه پیش</item>
|
||||||
|
<item quantity="other">%d ثانیه پیش</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="x_minutes_ago">
|
||||||
|
<item quantity="one">%d دقیقه پیش</item>
|
||||||
|
<item quantity="other">%d دقیقه پیش</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="edit_original_post">فرستهٔ اصلی</string>
|
||||||
|
<string name="edit_text_edited">متن ویرایش شد</string>
|
||||||
|
<string name="edit_spoiler_added">هشدار محتوا افزوده شد</string>
|
||||||
|
<string name="edit_spoiler_edited">هشدار محتوا ویرایش شد</string>
|
||||||
|
<string name="edit_spoiler_removed">هشدار محتوا برداشته شد</string>
|
||||||
|
<string name="edit_poll_added">نظرسنجی اضافه شد</string>
|
||||||
|
<string name="edit_poll_edited">نظرسنجی ویرایش شد</string>
|
||||||
|
<string name="edit_poll_removed">نظرسنجی برداشته شد</string>
|
||||||
|
<string name="edit_media_added">رسانه اضافه شد</string>
|
||||||
|
<string name="edit_media_removed">رسانه برداشته شد</string>
|
||||||
|
<string name="edit_multiple_changed">فرسته ویرایش شد</string>
|
||||||
<string name="edit">ویرایش</string>
|
<string name="edit">ویرایش</string>
|
||||||
|
<string name="upload_failed">بارگذاری ناموفق بود</string>
|
||||||
|
<string name="file_size_bytes">%d بایت</string>
|
||||||
|
<string name="file_size_kb">%.2f کیلوبایت</string>
|
||||||
|
<string name="file_size_mb">%.2f مگابایت</string>
|
||||||
|
<string name="file_size_gb">%.2f گیگابایت</string>
|
||||||
|
<string name="upload_processing">در حال پردازش…</string>
|
||||||
<!-- %s is version like 1.2.3 -->
|
<!-- %s is version like 1.2.3 -->
|
||||||
|
<string name="update_available">ماستودون برای اندروید %s آماده بارگیری است.</string>
|
||||||
<!-- %s is version like 1.2.3 -->
|
<!-- %s is version like 1.2.3 -->
|
||||||
|
<string name="update_ready">ماستودون برای اندروید %s بارگیری شده و آماده نصب است.</string>
|
||||||
<!-- %s is file size -->
|
<!-- %s is file size -->
|
||||||
|
<string name="download_update">بارگیری (%s)</string>
|
||||||
<string name="install_update">نصب</string>
|
<string name="install_update">نصب</string>
|
||||||
|
<string name="privacy_policy_title">حریم خصوصی شما</string>
|
||||||
<string name="i_agree">موافقم</string>
|
<string name="i_agree">موافقم</string>
|
||||||
|
<string name="text_copied">در تختهگیره رونوشت شد</string>
|
||||||
|
<string name="add_bookmark">نشانک</string>
|
||||||
|
<string name="remove_bookmark">برداشتن نشانک</string>
|
||||||
|
<string name="bookmarks">نشانکها</string>
|
||||||
|
<string name="your_favorites">علاقهمندی های شما</string>
|
||||||
|
<string name="server_filter_any_language">هر زبانی</string>
|
||||||
|
<string name="server_filter_region_europe">اروپا</string>
|
||||||
|
<string name="server_filter_region_north_america">آمریکای شمالی</string>
|
||||||
|
<string name="server_filter_region_south_america">آمریکای جنوبی</string>
|
||||||
|
<string name="server_filter_region_africa">آفریقا</string>
|
||||||
|
<string name="server_filter_region_asia">آسیا</string>
|
||||||
|
<string name="server_filter_region_oceania">اقیانوسیه</string>
|
||||||
|
<string name="profile_add_row">افزودن سطر</string>
|
||||||
|
<string name="profile_setup">تنظیم نمایه</string>
|
||||||
|
<string name="popular_on_mastodon">محبوب در ماستودون</string>
|
||||||
|
<string name="follow_all">پیگیری همه</string>
|
||||||
<!-- %s is server domain -->
|
<!-- %s is server domain -->
|
||||||
|
<string name="profile_bio">دربارهٔ شما</string>
|
||||||
<!-- Shown in a progress dialog when you tap "follow all" -->
|
<!-- Shown in a progress dialog when you tap "follow all" -->
|
||||||
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
|
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
|
||||||
|
<string name="spoiler_show">به هرصورت نشان داده شود</string>
|
||||||
|
<string name="spoiler_hide">نهفتن دوباره</string>
|
||||||
|
<string name="poll_multiple_choice">یک یا چند مورد را انتخاب کنید</string>
|
||||||
|
<string name="save_changes">ذخیرهٔ تغییرات</string>
|
||||||
|
<string name="profile_timeline">خط زمانی</string>
|
||||||
|
<string name="view_all">مشاهده همه</string>
|
||||||
|
<string name="profile_endorsed_accounts">حسابها</string>
|
||||||
|
<string name="verified_link">پیوند تأییدشده</string>
|
||||||
|
<string name="show">نمایش</string>
|
||||||
|
<string name="hide">نهفتن</string>
|
||||||
|
<string name="join_default_server">%s پیوست</string>
|
||||||
|
<string name="pick_server">انتخاب کارسازی دیگر</string>
|
||||||
|
<string name="signup_or_login">یا</string>
|
||||||
|
<string name="learn_more">بیشتر بیاموزید</string>
|
||||||
|
<string name="welcome_to_mastodon">به ماستودون خوش آمدید</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
73
mastodon/src/main/res/values-fa/strings_sk.xml
Normal file
73
mastodon/src/main/res/values-fa/strings_sk.xml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="sk_pinned_posts">سنجاق شده</string>
|
||||||
|
<string name="sk_confirm_pin_post_title">سنجاق فرسته به نمایه</string>
|
||||||
|
<string name="sk_app_name">مگالودون</string>
|
||||||
|
<string name="sk_pin_post">سنجاق به نمایه</string>
|
||||||
|
<string name="sk_pinning">درحال سنجاق کردن فرسته…</string>
|
||||||
|
<string name="sk_unpin_post">برداشتن سنجاق از نمایه</string>
|
||||||
|
<string name="sk_confirm_unpin_post_title">برداشتن سنجاق فرسته از نمایه</string>
|
||||||
|
<string name="sk_unpinning">درحال برداشتن سنجاق فرسته…</string>
|
||||||
|
<string name="sk_image_description">توضیحات تصویر</string>
|
||||||
|
<string name="sk_visibility_unlisted">فهرست نشده</string>
|
||||||
|
<string name="sk_settings_show_replies">نمایش پاسخها</string>
|
||||||
|
<string name="sk_list_timelines">سیاههها</string>
|
||||||
|
<string name="sk_notification_type_status">فرستهها</string>
|
||||||
|
<string name="sk_no_update_available">بهروزرسانی موجود نیست</string>
|
||||||
|
<string name="sk_settings_reply_visibility_all">همه پاسخها</string>
|
||||||
|
<string name="sk_mark_media_as_sensitive">علامتگذاری رسانه به عنوان حساس</string>
|
||||||
|
<string name="sk_update_available">مگالودون %s آماده بارگیری است.</string>
|
||||||
|
<string name="sk_follow_requests">درخواستهای پیگیری</string>
|
||||||
|
<string name="sk_accept_follow_request">پذیرفتن درخواست پیگیری</string>
|
||||||
|
<string name="sk_reject_follow_request">رد درخواست پیگیری</string>
|
||||||
|
<string name="sk_notify_posts">اعلانهای فرسته</string>
|
||||||
|
<string name="sk_color_palette_material3">سامانه</string>
|
||||||
|
<string name="sk_color_palette_pink">صورتی</string>
|
||||||
|
<string name="sk_color_palette_purple">بنفش</string>
|
||||||
|
<string name="sk_color_palette_green">سبز</string>
|
||||||
|
<string name="sk_color_palette_blue">آبی</string>
|
||||||
|
<string name="sk_color_palette_brown">قهوهای</string>
|
||||||
|
<string name="sk_color_palette_red">قرمز</string>
|
||||||
|
<string name="sk_color_palette_yellow">زرد</string>
|
||||||
|
<string name="sk_translate_post">ترجمه</string>
|
||||||
|
<string name="sk_post_language">زبان: %s</string>
|
||||||
|
<string name="sk_available_languages">زبان های موجود</string>
|
||||||
|
<string name="sk_welcome_title">خوش آمدید!</string>
|
||||||
|
<string name="sk_language_name">%1$s (%2$s)</string>
|
||||||
|
<string name="sk_settings_profile">تنظیم نمایه</string>
|
||||||
|
<string name="sk_settings_filters">پیکربندی پالایه</string>
|
||||||
|
<string name="sk_settings_auth">تنظیمات امنیتی</string>
|
||||||
|
<string name="sk_settings_about">درباره کاره</string>
|
||||||
|
<string name="sk_settings_donate">اعانه</string>
|
||||||
|
<string name="sk_clear_all_notifications">پاکسازی همه اعلانات</string>
|
||||||
|
<string name="sk_clear_all_notifications_confirm_action">حذف همه</string>
|
||||||
|
<string name="sk_settings_publish_button_text">متن دکمه انتشار</string>
|
||||||
|
<string name="sk_settings_publish_button_text_title">سفارشیسازی متن دکمه انتشار</string>
|
||||||
|
<string name="sk_undo_reblog">برگردان تقویت</string>
|
||||||
|
<string name="sk_settings_rules">قوانین</string>
|
||||||
|
<string name="sk_collapse">بستن</string>
|
||||||
|
<string name="sk_expand">باز کردن</string>
|
||||||
|
<string name="sk_notify_poll_results">نتایج نظرسنجی</string>
|
||||||
|
<string name="sk_settings_server_version">نسخه کارساز: %s</string>
|
||||||
|
<string name="sk_settings_glitch_instance">حالت فقط محلی Glitch</string>
|
||||||
|
<string name="sk_inline_local_only">فقط محلی</string>
|
||||||
|
<string name="sk_updater_enable_pre_releases">به کار انداختن پیش انتشار</string>
|
||||||
|
<string name="sk_save_draft">ذخیره پیش نویس؟</string>
|
||||||
|
<string name="sk_no_results">بدون نتیجه</string>
|
||||||
|
<string name="sk_searching">درحال جستجو…</string>
|
||||||
|
<string name="sk_attach_file">پیوست پرونده</string>
|
||||||
|
<string name="sk_post_edited">ویرایش شده</string>
|
||||||
|
<string name="sk_content_type_markdown">مارکدون</string>
|
||||||
|
<string name="sk_content_type_html">HTML</string>
|
||||||
|
<string name="sk_content_type_plain">متن ساده</string>
|
||||||
|
<string name="sk_check_for_update">بررسی برای بهروزرسانی</string>
|
||||||
|
<string name="sk_settings_support_local_only">کارساز از فرسته فقط محلی پشتیبانی میکند</string>
|
||||||
|
<string name="sk_settings_show_boosts">نمایش تقویتها</string>
|
||||||
|
<string name="sk_notification_type_update">فرستههای ویرایش شده</string>
|
||||||
|
<string name="sk_update_ready">مگالودون %s بارگیری شده و آماده نصب است.</string>
|
||||||
|
<string name="sk_poll_allow_multiple">اجازه انتخاب های متعدد</string>
|
||||||
|
<string name="sk_open_with_account">باز کردن با حساب دیگر</string>
|
||||||
|
<string name="sk_confirm_delete_draft_title">حذف پیش نویس</string>
|
||||||
|
<string name="sk_draft">پیشنویس</string>
|
||||||
|
<string name="sk_draft_saved">پیش نویس ذخیره شد</string>
|
||||||
|
</resources>
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
<string name="sk_expand">Développer</string>
|
<string name="sk_expand">Développer</string>
|
||||||
<string name="sk_collapse">Réduire</string>
|
<string name="sk_collapse">Réduire</string>
|
||||||
<string name="sk_settings_collapse_long_posts">Réduire les messages très longs</string>
|
<string name="sk_settings_collapse_long_posts">Réduire les messages très longs</string>
|
||||||
<string name="sk_settings_prefix_reply_cw_with_re">Préfixe \"re :\" lors d\'une réponse avec AC</string>
|
<string name="sk_settings_prefix_reply_cw_with_re">Préfixe les AC avec \"re :\" sur les réponses à</string>
|
||||||
<string name="sk_filtered">Filtré : %s</string>
|
<string name="sk_filtered">Filtré : %s</string>
|
||||||
<string name="sk_unfinished_attachments">Corriger les pièces jointes \?</string>
|
<string name="sk_unfinished_attachments">Corriger les pièces jointes \?</string>
|
||||||
<string name="sk_unfinished_attachments_message">Certaines pièces jointes n\'ont pas fini de se télécharger.</string>
|
<string name="sk_unfinished_attachments_message">Certaines pièces jointes n\'ont pas fini de se télécharger.</string>
|
||||||
@@ -290,7 +290,20 @@
|
|||||||
<string name="sk_open_in_app">Ouvrir dans l\'application</string>
|
<string name="sk_open_in_app">Ouvrir dans l\'application</string>
|
||||||
<string name="sk_external_share_title">Partager avec le compte</string>
|
<string name="sk_external_share_title">Partager avec le compte</string>
|
||||||
<string name="sk_external_share_or_open_title">Partager ou ouvrir avec le compte</string>
|
<string name="sk_external_share_or_open_title">Partager ou ouvrir avec le compte</string>
|
||||||
<string name="sk_bubble_timeline_info_banner">Ce sont les publications les plus récentes des personnes présentes dans la bulle de votre serveur Akkoma.</string>
|
<string name="sk_bubble_timeline_info_banner">Ce sont les messages les plus récents du réseau organisés par vos administrateurs d\'instance.</string>
|
||||||
<string name="sk_timeline_bubble">Bulle</string>
|
<string name="sk_timeline_bubble">Bulle</string>
|
||||||
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</string>
|
<string name="sk_instance_info_unavailable">Informations sur l\'instance temporairement indisponibles</string>
|
||||||
|
<string name="sk_open_in_app_failed">Impossible de l\'ouvrir dans l\'application</string>
|
||||||
|
<string name="sk_settings_allow_remote_loading">Charger des informations à partir d\'instances distantes</string>
|
||||||
|
<string name="sk_no_remote_info_hint">informations distantes indisponibles</string>
|
||||||
|
<string name="sk_error_loading_profile">Échec du chargement du profil via %s</string>
|
||||||
|
<string name="sk_settings_allow_remote_loading_explanation">Essayez de récupérer des listes plus précises pour les abonnés, les likes et les boosts en chargeant les informations à partir de l\'instance d\'origine.</string>
|
||||||
|
<string name="sk_settings_auto_reveal_equal_spoilers">Révéler les AC identiques dans les réponses de</string>
|
||||||
|
<string name="sk_settings_auto_reveal_nobody">personne</string>
|
||||||
|
<string name="sk_settings_auto_reveal_author">auteur</string>
|
||||||
|
<string name="sk_settings_auto_reveal_anyone">tout le monde</string>
|
||||||
|
<string name="sk_settings_prefix_replies_always">tout le monde</string>
|
||||||
|
<string name="sk_settings_prefix_replies_never">personne</string>
|
||||||
|
<string name="sk_settings_prefix_replies_to_others">autres</string>
|
||||||
|
<string name="sk_settings_forward_report_default">\"Transférer le rapport\" par défaut</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<string name="sk_loading_fediverse_resource_title">Buscando no Fediverso</string>
|
<string name="sk_loading_fediverse_resource_title">Buscando no Fediverso</string>
|
||||||
<string name="sk_reblog_with_visibility">Impulsar con visibilidade</string>
|
<string name="sk_reblog_with_visibility">Impulsar con visibilidade</string>
|
||||||
<string name="sk_quote_post">Publicar acerca disto</string>
|
<string name="sk_quote_post">Publicar acerca disto</string>
|
||||||
<string name="sk_undo_reblog">Desfacer o impulso</string>
|
<string name="sk_undo_reblog">Desfacer impulso</string>
|
||||||
<string name="sk_copy_link_to_post">Copiar ligazón á publicación</string>
|
<string name="sk_copy_link_to_post">Copiar ligazón á publicación</string>
|
||||||
<string name="sk_loading_resource_on_instance_title">Buscando en %s</string>
|
<string name="sk_loading_resource_on_instance_title">Buscando en %s</string>
|
||||||
<string name="sk_open_with_account">Abrir con outra conta</string>
|
<string name="sk_open_with_account">Abrir con outra conta</string>
|
||||||
@@ -286,4 +286,10 @@
|
|||||||
<string name="sk_content_type">Tipo de contido</string>
|
<string name="sk_content_type">Tipo de contido</string>
|
||||||
<string name="sk_content_type_markdown">Markdown</string>
|
<string name="sk_content_type_markdown">Markdown</string>
|
||||||
<string name="sk_settings_content_types_explanation">Permite configurar un tipo de contido como Markdown ao crear unha publicación. Teña en conta que non tódalas instancias soportan isto.</string>
|
<string name="sk_settings_content_types_explanation">Permite configurar un tipo de contido como Markdown ao crear unha publicación. Teña en conta que non tódalas instancias soportan isto.</string>
|
||||||
|
<string name="sk_bubble_timeline_info_banner">Estas son as publicacións máis recentes da xente na burbulla do seu servidor Akkoma.</string>
|
||||||
|
<string name="sk_timeline_bubble">Burbulla</string>
|
||||||
|
<string name="sk_instance_info_unavailable">Información da instancia temporalmente non dispoñible</string>
|
||||||
|
<string name="sk_open_in_app">Abrir na aplicación</string>
|
||||||
|
<string name="sk_external_share_title">Compartir coa conta</string>
|
||||||
|
<string name="sk_external_share_or_open_title">Compartir ou abrir coa conta</string>
|
||||||
</resources>
|
</resources>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user