Compare commits

..

98 Commits

Author SHA1 Message Date
sk
3d79e87ec8 Merge remote-tracking branch 'upstream/l10n_master' 2023-06-12 20:43:22 +02:00
sk
09ed3c647a reduce max height for photos 2023-06-12 20:25:12 +02:00
EndermanCo
172515ba0a Translated using Weblate (Persian)
Currently translated at 23.2% (71 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-06-12 17:43:48 +00:00
Andrewblasco
a2f0fc8c87 Translated using Weblate (Spanish)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/es/
2023-06-12 17:43:48 +00:00
Andrewblasco
888cee4556 Translated using Weblate (Spanish)
Currently translated at 100.0% (305 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-12 17:43:48 +00:00
sk
c005e9bf18 apply upstream alt overlay changes 2023-06-12 19:43:13 +02:00
Eugen Rochko
64362968fc New translations strings.xml (Persian) 2023-06-12 19:10:22 +02:00
sk
320027ca9b merge upstream photo layout changes 2023-06-12 18:42:07 +02:00
sk
ad2678da7c Merge remote-tracking branch 'upstream/l10n_master' 2023-06-12 18:22:38 +02:00
sk22
c56c7448d0 Translated using Weblate (German)
Currently translated at 100.0% (18 of 18 strings)

Translation: Megalodon/metadata
Translate-URL: https://translate.codeberg.org/projects/megalodon/metadata/de/
2023-06-12 16:22:10 +00:00
sk
731e67725c fix errors in strings 2023-06-12 18:22:02 +02:00
sk
8178f81c85 update changelog 2023-06-12 18:08:40 +02:00
sk
6fbf00a132 add newline 2023-06-12 18:07:28 +02:00
sk
968cde9e4c clean up empty regional locales 2023-06-12 18:03:22 +02:00
sk
f2ab2acef7 Merge remote-tracking branch 'weblate/main' 2023-06-12 17:47:22 +02:00
sk
81d1ecc5f8 update readme, changelog 2023-06-12 17:47:10 +02:00
EndermanCo
513e6439ff Translated using Weblate (Persian)
Currently translated at 22.2% (68 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fa/
2023-06-12 15:35:16 +00:00
Oliebol
1f0108b14e Translated using Weblate (Dutch)
Currently translated at 84.5% (258 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-06-12 15:35:15 +00:00
Choukajohn
1433d0717e Translated using Weblate (French)
Currently translated at 100.0% (305 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-12 15:35:15 +00:00
sk
be91775f4b fix pinned posts showing post multiple times 2023-06-12 17:29:00 +02:00
sk
7b2f8d2be3 update readme 2023-06-12 16:40:52 +02:00
sk
54c386ccec update readme 2023-06-12 16:32:28 +02:00
sk
9d9e98959f change glitch url 2023-06-12 16:18:45 +02:00
sk
a3cd7224bd update readme 2023-06-12 16:17:08 +02:00
sk
6cf214f127 fix header disappearing in some cases
closes sk22#570
2023-06-12 15:39:38 +02:00
Eugen Rochko
b6976fb519 New translations strings.xml (Persian) 2023-06-12 15:36:23 +02:00
sk
871ada23ab don't display context until fragment transition finished 2023-06-12 14:33:09 +02:00
Eugen Rochko
040237de2b New translations strings.xml (Persian) 2023-06-12 14:30:32 +02:00
sk
29b2a25840 no error when opening url that looks like fedi url 2023-06-12 13:33:30 +02:00
sk
bd2f05be67 hide followers/following count if count is -1 2023-06-12 12:13:44 +02:00
sk22
ed075b276f Translated using Weblate (German)
Currently translated at 100.0% (305 of 305 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-11 23:48:58 +00:00
sk
dd25f3380a add setting to configure default value for "forward report"
closes sk22#565
2023-06-12 01:38:44 +02:00
sk22
1a2d1efa29 Added translation using Weblate (Persian) 2023-06-11 23:10:35 +00:00
sk
f3e1fa4b2b fix announcements loading infinitely
closes sk22#569
2023-06-12 01:01:45 +02:00
Eugen Rochko
fc307ff43f New translations strings.xml (Persian) 2023-06-11 21:52:27 +02:00
Eugen Rochko
04304b3397 New translations strings.xml (Persian) 2023-06-11 20:49:08 +02:00
sk
23f4b63195 Merge remote-tracking branch 'upstream/l10n_master' 2023-06-11 20:04:33 +02:00
sk22
edeae13dda Translated using Weblate (German)
Currently translated at 100.0% (304 of 304 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-11 18:04:03 +00:00
Oliebol
44558534e9 Translated using Weblate (Dutch)
Currently translated at 85.0% (256 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/nl/
2023-06-11 17:49:51 +00:00
Linerly
2b760bb215 Translated using Weblate (Indonesian)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-11 17:49:51 +00:00
sk
44154a987d setting for "re:" reply CW prefix always or only to others
re: sk22#567
2023-06-11 19:46:18 +02:00
Eugen Rochko
7260db6668 New translations full_description.txt (Persian) 2023-06-11 19:41:48 +02:00
Eugen Rochko
1dadc51ddf New translations strings.xml (Persian) 2023-06-11 19:41:48 +02:00
Eugen Rochko
fe85351869 New translations strings.xml (Persian) 2023-06-11 18:34:16 +02:00
Eugen Rochko
74a83c6ac4 New translations strings.xml (Persian) 2023-06-11 17:37:10 +02:00
sk
acb1369e88 fix "show replies/boosts" filter not applied
closes sk22#566
2023-06-11 17:23:54 +02:00
sk
8e0d74f9c2 use appkit V.sp 2023-06-11 17:12:56 +02:00
Eugen Rochko
df77ba61ad New translations strings.xml (Persian) 2023-06-11 15:12:30 +02:00
Eugen Rochko
ed40f74d59 New translations strings.xml (Persian) 2023-06-11 13:57:15 +02:00
Eugen Rochko
42e26bef68 New translations strings.xml (Persian) 2023-06-11 12:54:18 +02:00
Eugen Rochko
af60adb55f New translations strings.xml (Indonesian) 2023-06-11 05:16:01 +02:00
sk
b94741feae boop version 2023-06-10 22:10:24 +02:00
sk
e43d6c35d8 update languages 2023-06-10 22:10:13 +02:00
sk
4a6f9e80b1 Merge remote-tracking branch 'weblate/main' 2023-06-10 22:03:41 +02:00
gallegonovato
ec02680507 Translated using Weblate (Spanish)
Currently translated at 99.6% (300 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/es/
2023-06-10 20:03:34 +00:00
sk22
5fc569a45a Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 20:03:34 +00:00
sk
4bc9c5691d Merge remote-tracking branch 'upstream/l10n_master' 2023-06-10 22:03:19 +02:00
sk
19b68855ac move permission definition to status privacy 2023-06-10 21:54:47 +02:00
sk
70fdfb612e fix issue on re-bind 2023-06-10 21:54:18 +02:00
sk
0a32c217d8 Merge branch 'main' into pr/FineFindus/557 2023-06-10 21:47:26 +02:00
Eugen Rochko
5dfa9237ad New translations short_description.txt (Persian) 2023-06-10 21:44:47 +02:00
sk
573ff75498 update ancestor when deleting post 2023-06-10 21:32:41 +02:00
sk
87c37df370 insert replied directly below status
closes sk22#558
2023-06-10 20:57:01 +02:00
sk22
7fb0944e66 Translated using Weblate (German)
Currently translated at 100.0% (301 of 301 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/de/
2023-06-10 16:59:16 +00:00
sk
35c8a3d121 change strings 2023-06-10 18:59:05 +02:00
sk
9e58413d1a Merge remote-tracking branch 'weblate/main' 2023-06-10 18:54:32 +02:00
sk
90e60aef84 change auto-reveal cw wording
closes sk22#562
2023-06-10 18:53:38 +02:00
sk
8547ce05ed change auto-reveal cw wording
closes sk22#562
2023-06-10 18:52:01 +02:00
gicorada
0825faee5c Translated using Weblate (Italian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/it/
2023-06-10 16:42:23 +00:00
Linerly
d43a697df7 Translated using Weblate (Indonesian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/id/
2023-06-10 16:42:23 +00:00
Choukajohn
3b742c4391 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
ihor_ck
a43a396043 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/uk/
2023-06-10 16:42:23 +00:00
Choukajohn
bcb4fac553 Translated using Weblate (French)
Currently translated at 100.0% (297 of 297 strings)

Translation: Megalodon/values
Translate-URL: https://translate.codeberg.org/projects/megalodon/values/fr/
2023-06-10 16:42:23 +00:00
sk
35bf858a83 auto-reveal equal spoilers in threads 2023-06-09 14:54:03 +02:00
sk
870bfaf08c don't use switch for android ids 2023-06-09 14:50:21 +02:00
sk
c4238fb19b keep revealed states when reloading
closes sk22#561
2023-06-09 13:27:25 +02:00
sk
ba7aeb358b fix thread status not clickable if filter revealed
closes sk22#554
2023-06-09 12:50:10 +02:00
sk
6f3fd4d454 fix opening browser twice
closes sk22#559
2023-06-09 12:47:32 +02:00
Eugen Rochko
c890195567 New translations strings.xml (Swedish) 2023-06-08 22:57:52 +02:00
Eugen Rochko
b50a327b17 New translations strings.xml (Swedish) 2023-06-08 17:47:06 +02:00
sk
97547f334f returning optionals for the optional god
closes sk22#555
2023-06-08 15:31:58 +02:00
sk
1ab953d819 fix spoiler button being hidden while editing
closes sk22#553
2023-06-08 15:03:04 +02:00
FineFindus
dbe7eb25ff Merge branch 'main' into feat/hide-non-boostable-boosts 2023-06-08 10:35:12 +02:00
FineFindus
45ecec09f5 feat: hide boost count on non-boostable statuses 2023-06-08 10:33:27 +02:00
FineFindus
9b4556d293 refactor: move boostable check to status 2023-06-08 10:33:27 +02:00
sk
307d483a56 add comment 2023-06-07 21:59:56 +02:00
sk
9612248695 remove unused imports 2023-06-07 21:51:17 +02:00
sk
1f63401e5b fix pixelfed post editing 2023-06-07 21:50:50 +02:00
sk
d35ec18a88 increase akkoma scheduled posts compatibility 2023-06-07 21:12:38 +02:00
sk
b93b1847c3 increase pixelfed compatibility 2023-06-07 21:12:30 +02:00
sk
cd46ed565f open browser if login redirects to website 2023-06-07 21:11:20 +02:00
sk
4a0e4edef8 load more if screen isn't full yet 2023-06-07 20:28:56 +02:00
sk
2ea7333daa only reload main status when refreshing
closes sk22#552
2023-06-07 18:45:50 +02:00
sk
fa7a66809d change profile loading error behavior 2023-06-07 16:09:41 +02:00
sk
71884ab760 don't show wrong relationship with remote accounts 2023-06-07 15:50:30 +02:00
sk
f31205c670 generalize lookup error handling 2023-06-07 15:45:22 +02:00
sk
0091ae87ce fix issues with external share
closes sk22#550
2023-06-07 15:17:44 +02:00
sk
ad13b1e927 bump version 2023-06-06 17:13:30 +02:00
80 changed files with 1225 additions and 513 deletions

View File

@@ -10,44 +10,47 @@
 
<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 wont 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
### **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 peoples 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).
</details>
### **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 wasnt included in the official Mastodon app supposedly, because this conflicts with Googles safety requirements for apps on the Play Store.
Thats 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 Megalodons 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**
**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.
### **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 youre sharing is as accessible as possible** to people who cant see the images and rely on software to read back the provided content descriptions. Thankfully, its 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 peoples profiles shows all the posts they pinned.**
On the Fediverse, its 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.
</details>
## Installation
@@ -88,7 +91,7 @@ Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastod
## 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 dont want that, just download the [latest full release](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk).
**`megalodon.apk`**
@@ -108,11 +111,11 @@ Variant without the integrated updater. This is the variant to be published to F
### 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)
[![Translation status](https://translate.codeberg.org/widgets/megalodon/-/horizontal-auto.svg)](https://translate.codeberg.org/engage/megalodon/)
[![Translation status](https://translate.codeberg.org/widgets/megalodon/-/horizontal-auto.svg)](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)
* [Pinnable custom timelines](https://github.com/sk22/megalodon/pull/338/commits)
* Support for local-only posts
* Support for copying the URL to posts/accounts/… in Pixel launchers Recent apps view
* Compatibility for Akkoma Bubble timeline
* Listings of followers/following/favorites/boosts can be loaded from the origin instance (theres 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
@@ -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)
* [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)
* [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
@@ -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
* Header in timeline for followed hashtags
* [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
@@ -206,6 +216,8 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
./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 Androids developer. In case the used AppKit version isnt 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
This project is released under the [GPL-3 License](./LICENSE).

View 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

View File

@@ -15,8 +15,8 @@ android {
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 33
versionCode 90
versionName "1.2.3+fork.90"
versionCode 92
versionName "1.2.3+fork.92"
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']
}

View File

@@ -9,6 +9,7 @@ import android.text.TextUtils;
import android.util.Pair;
import android.widget.Toast;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
@@ -43,7 +44,7 @@ public class ExternalShareActivity extends FragmentStackActivity{
finish();
} else if (isOpenable || sessions.size() > 1) {
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
if (isOpenable) sheet.setOnClick((accountId, open) -> {
sheet.setOnClick((accountId, open) -> {
if (open && text.isPresent()) {
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
if (clazz == null) {
@@ -59,8 +60,17 @@ public class ExternalShareActivity extends FragmentStackActivity{
finish();
startActivity(intent);
};
if (isFediUrl) UiUtils.lookupURL(this, accountId, text.get(), false, callback);
else UiUtils.lookupAccountHandle(this, accountId, fediHandle.get(), callback);
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 {
openComposeFragment(accountId);
}

View File

@@ -40,7 +40,7 @@ public class GlobalUserPreferences{
public static boolean showAltIndicator;
public static boolean showNoAltIndicator;
public static boolean enablePreReleases;
public static boolean prefixRepliesWithRe;
public static PrefixRepliesMode prefixReplies;
public static boolean bottomEncoding;
public static boolean collapseLongPosts;
public static boolean spectatorMode;
@@ -49,13 +49,12 @@ public class GlobalUserPreferences{
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static boolean allowRemoteLoading;
public static boolean forwardReportDefault;
public static AutoRevealMode autoRevealEqualSpoilers;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
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<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport;
@@ -63,6 +62,10 @@ public class GlobalUserPreferences{
public static Set<String> accountsWithContentTypesEnabled;
public static Map<String, ContentType> accountsDefaultContentTypes;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
private final static Type accountsDefaultContentTypesType = new TypeToken<Map<String, ContentType>>() {}.getType();
/**
* Pleroma
*/
@@ -111,7 +114,7 @@ public class GlobalUserPreferences{
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false);
prefixReplies=PrefixRepliesMode.valueOf(prefs.getString("prefixReplies", PrefixRepliesMode.NEVER.name()));
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
spectatorMode=prefs.getBoolean("spectatorMode", false);
@@ -129,6 +132,17 @@ public class GlobalUserPreferences{
accountsWithContentTypesEnabled=prefs.getStringSet("accountsWithContentTypesEnabled", new HashSet<>());
accountsDefaultContentTypes=fromJson(prefs.getString("accountsDefaultContentTypes", null), accountsDefaultContentTypesType, new HashMap<>());
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
if (prefs.contains("prefixRepliesWithRe")) {
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
? PrefixRepliesMode.TO_OTHERS : PrefixRepliesMode.NEVER;
prefs.edit()
.putString("prefixReplies", prefixReplies.name())
.remove("prefixRepliesWithRe")
.apply();
}
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
@@ -160,7 +174,7 @@ public class GlobalUserPreferences{
.putBoolean("showAltIndicator", showAltIndicator)
.putBoolean("showNoAltIndicator", showNoAltIndicator)
.putBoolean("enablePreReleases", enablePreReleases)
.putBoolean("prefixRepliesWithRe", prefixRepliesWithRe)
.putString("prefixReplies", prefixReplies.name())
.putBoolean("collapseLongPosts", collapseLongPosts)
.putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab)
@@ -179,6 +193,8 @@ public class GlobalUserPreferences{
.putStringSet("accountsWithContentTypesEnabled", accountsWithContentTypesEnabled)
.putString("accountsDefaultContentTypes", gson.toJson(accountsDefaultContentTypes))
.putBoolean("allowRemoteLoading", allowRemoteLoading)
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
.putBoolean("forwardReportDefault", forwardReportDefault)
.apply();
}
@@ -198,4 +214,16 @@ public class GlobalUserPreferences{
LIGHT,
DARK
}
public enum AutoRevealMode {
NEVER,
THREADS,
DISCUSSIONS
}
public enum PrefixRepliesMode {
NEVER,
ALWAYS,
TO_OTHERS
}
}

View File

@@ -127,8 +127,8 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
}
private void showFragmentForExternalShare(Bundle args) {
String clazz = args.getString("fromExternalShare");
Fragment fragment = switch (clazz) {
String className = args.getString("fromExternalShare");
Fragment fragment = switch (className) {
case "ThreadFragment" -> new ThreadFragment();
case "ProfileFragment" -> new ProfileFragment();
default -> null;

View File

@@ -61,6 +61,9 @@ public class OAuthActivity extends Activity{
@Override
public void onSuccess(Token token){
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<>(){
@Override
public void onSuccess(Account account){

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android;
import static org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode.*;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@@ -295,7 +297,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
req.language = preferences.postingDefaultLanguage;
req.visibility = preferences.postingDefaultVisibility;
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;
}

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -161,6 +162,11 @@ public class MastodonAPIController{
respObj=gson.fromJson(reader, req.respClass);
}
}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)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage(), response.code(), x);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.api;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
@@ -50,6 +51,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Map<String, String> headers;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@Nullable Context context;
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
this.path=path;
@@ -181,6 +183,16 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
return this;
}
public MastodonAPIRequest<T> setContext(Context context) {
this.context = context;
return this;
}
@Nullable
public Context getContext() {
return context;
}
@CallSuper
public void validateAndPostprocessResponse(T respObj, Response httpResponse) throws IOException{
if(respObj instanceof BaseModel){

View File

@@ -94,12 +94,15 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
@Override
public void onSuccess(List<Announcement> result){
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());
onDataLoaded(unread, true);
onDataLoaded(read, false);
if (unread.isEmpty()) setResult(true, null);
else unreadIDs = unread.stream().map(a -> a.id).collect(toList());
// get unread items first
List<Announcement> data = result.stream().filter(a -> !a.read).collect(toList());
if (data.isEmpty()) setResult(true, null);
else unreadIDs = data.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);

View File

@@ -754,6 +754,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
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{
public DisplayItemsAdapter(){

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode.*;
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
@@ -149,7 +150,7 @@ import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
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 IMAGE_DESCRIPTION_RESULT=363;
@@ -355,6 +356,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} else {
mediaBtn.setOnClickListener(v -> openFilePicker(false));
}
if (isInstancePixelfed()) pollBtn.setVisibility(View.GONE);
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
@@ -745,9 +747,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(!TextUtils.isEmpty(status.spoilerText)){
hasSpoiler=true;
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);
}else{
} else {
spoilerEdit.setText(status.spoilerText);
}
spoilerBtn.setSelected(true);
@@ -847,12 +851,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
if (editingStatus != null && scheduledStatus == null) {
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
// editing an already published post
draftsBtn.setVisibility(View.GONE);
}
}
@Override
public String getAccountID() {
return accountID;
}
private void navigateToUnsentPosts() {
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -1009,7 +1019,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.state!=AttachmentUploadState.DONE)
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);
}
@@ -1151,7 +1161,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
sendProgress.setVisibility(View.VISIBLE);
sendError.setVisibility(View.GONE);
Callback<Status> resCallback=new Callback<>(){
Callback<Status> resCallback = new Callback<>(){
@Override
public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> {
@@ -1164,7 +1174,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
E.post(new StatusCountersUpdatedEvent(replyTo));
}
}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()) {
Nav.finish(ComposeFragment.this);
@@ -1899,9 +1919,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu();
if (isInstancePixelfed()) {
m.findItem(R.id.vis_private).setVisible(false);
}
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.isAkkoma()) {
if (isInstanceAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);

View File

@@ -255,6 +255,10 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
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);
if(relationship == null || !relationship.followedBy){
actionWrap.setVisibility(View.GONE);

View File

@@ -17,6 +17,10 @@ public interface HasAccountID {
return getInstance().map(Instance::isAkkoma).orElse(false);
}
default boolean isInstancePixelfed() {
return getInstance().map(Instance::isPixelfed).orElse(false);
}
default Optional<Instance> getInstance() {
return getSession().getInstance();
}

View File

@@ -48,11 +48,13 @@ public class HomeTimelineFragment extends StatusListFragment {
loadData();
}
private boolean typeFilterPredicate(Status s) {
return (GlobalUserPreferences.showReplies || s.inReplyToId == null) &&
(GlobalUserPreferences.showBoosts || s.reblog == null);
}
private List<Status> filterPosts(List<Status> items) {
return items.stream().filter(i ->
(GlobalUserPreferences.showReplies || i.inReplyToId == null) &&
(GlobalUserPreferences.showBoosts || i.reblog == null)
).collect(Collectors.toList());
return items.stream().filter(this::typeFilterPredicate).collect(Collectors.toList());
}
@Override
@@ -232,7 +234,7 @@ public class HomeTimelineFragment extends StatusListFragment {
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
if(filterPredicate.test(s)){
if(typeFilterPredicate(s) && filterPredicate.test(s)){
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}

View File

@@ -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);
}
}

View File

@@ -124,7 +124,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ProgressBarButton actionButton, notifyButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private PinnedPostsListFragment pinnedPostsFragment;
// private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
@@ -384,10 +385,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
UiUtils.lookupAccountHandle(getContext(), accountID, remoteAccount.getFullyQualifiedName(), (c, args) -> {
if (getContext() == null) return;
if (args == null || !args.containsKey("profileAccount")) {
onError(new MastodonErrorResponse(
getContext().getString(R.string.sk_error_loading_profile),
0, null
));
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")));
@@ -421,8 +422,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
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);
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();
setFields(fields);
}
@@ -565,6 +572,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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)));
if (account.followersCount < 0) followersBtn.setVisibility(View.GONE);
if (account.followingCount < 0) followingBtn.setVisibility(View.GONE);
UiUtils.loadCustomEmojiInTextView(name);
UiUtils.loadCustomEmojiInTextView(bio);
@@ -654,8 +664,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
));
}
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;
}
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));

View File

@@ -37,7 +37,9 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.GlobalUserPreferences.AutoRevealMode;
import org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import org.joinmastodon.android.GlobalUserPreferences.PrefixRepliesMode;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@@ -85,8 +87,8 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem;
private ButtonItem defaultContentTypeButtonItem;
private SwitchItem showNewPostsItem, glitchModeItem, compactReblogReplyLineItem, alwaysRevealSpoilersItem;
private ButtonItem defaultContentTypeButtonItem, autoRevealSpoilersItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private boolean needAppRestart;
@@ -189,9 +191,18 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.showInteractionCounts=i.checked;
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.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->{
GlobalUserPreferences.disableSwipe=i.checked;
@@ -211,14 +222,27 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
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->{
GlobalUserPreferences.prefixRepliesWithRe=i.checked;
items.add(new ButtonItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, b->{
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();
}));
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.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();
@@ -276,7 +300,7 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
GlobalUserPreferences.collapseLongPosts=i.checked;
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.save();
needAppRestart=true;
@@ -531,6 +555,52 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
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){
GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save();
@@ -560,14 +630,14 @@ public class SettingsFragment extends MastodonToolbarFragment implements Provide
private boolean onContentTypeChanged(MenuItem item, Button btn){
int id = item.getItemId();
ContentType contentType = switch (id) {
case R.id.content_type_plain -> ContentType.PLAIN;
case R.id.content_type_html -> ContentType.HTML;
case R.id.content_type_markdown -> ContentType.MARKDOWN;
case R.id.content_type_bbcode -> ContentType.BBCODE;
case R.id.content_type_misskey_markdown -> ContentType.MISSKEY_MARKDOWN;
default -> null;
};
ContentType contentType = null;
if (id == R.id.content_type_plain) contentType = ContentType.PLAIN;
else if (id == R.id.content_type_html) contentType = ContentType.HTML;
else if (id == R.id.content_type_markdown) contentType = ContentType.MARKDOWN;
else if (id == R.id.content_type_bbcode) contentType = ContentType.BBCODE;
else if (id == R.id.content_type_misskey_markdown) contentType = ContentType.MISSKEY_MARKDOWN;
GlobalUserPreferences.accountsDefaultContentTypes.put(accountID, contentType);
GlobalUserPreferences.save();
btn.setText(getContentTypeString(contentType));

View File

@@ -164,13 +164,29 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
protected void removeStatus(Status status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
int index=-1, ancestorFirstIndex = -1, ancestorLastIndex = -1;
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;
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)
return;
int lastIndex;

View File

@@ -8,6 +8,8 @@ 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.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
@@ -37,6 +39,7 @@ import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -49,7 +52,8 @@ import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus, updatedStatus;
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
protected boolean contextInitiallyRendered;
private StatusContext result;
protected boolean contextInitiallyRendered, transitionFinished;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -102,66 +106,29 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
footer.hideCounts=true;
}
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
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;
}
@Override
public void onTransitionFinished() {
transitionFinished = true;
maybeApplyContext();
}
@Override
protected void doLoadData(int offset, int count){
loadMainStatus();
if (refreshing) loadMainStatus();
currentRequest=new GetStatusContext(mainStatus.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(StatusContext result){
if (getContext() == null) return;
if(refreshing){
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);
// 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();
}
ThreadFragment.this.result = result;
maybeApplyContext();
}
})
.exec(accountID);
@@ -184,9 +151,84 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}).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 &&
@@ -310,15 +352,65 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}
protected void onStatusCreated(StatusCreatedEvent ev){
if(ev.status.inReplyToId!=null && getStatusByID(ev.status.inReplyToId)!=null){
data.add(ev.status);
onAppendItems(Collections.singletonList(ev.status));
if (ev.status.inReplyToId == null) return;
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
public boolean isItemEnabled(String id){
return !id.equals(mainStatus.id);
return !id.equals(mainStatus.id) || !mainStatus.filterRevealed;
}
@Override

View File

@@ -257,7 +257,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void bindRelationship(){
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);
}else{
button.setVisibility(View.VISIBLE);

View File

@@ -243,6 +243,10 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
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);
if(relationship==null){
actionWrap.setVisibility(View.GONE);

View File

@@ -15,6 +15,7 @@ import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.reports.SendReport;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
@@ -39,7 +40,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
private TextView forwardReportText;
private Switch forwardReportSwitch;
private EditText commentEdit;
private boolean forwardReport;
private boolean forwardReport = GlobalUserPreferences.forwardReportDefault;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -89,7 +90,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
} else {
forwardReportItem.setOnClickListener(this::onForwardReportClick);
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
forwardReportSwitch.setChecked(forwardReport = true);
forwardReportSwitch.setChecked(forwardReport);
}
return view;
}

View File

@@ -65,7 +65,6 @@ public class Account extends BaseModel implements Searchable{
/**
* An image banner that is shown above the profile and in profile cards.
*/
@RequiredField
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.

View File

@@ -148,6 +148,10 @@ public class Instance extends BaseModel{
return pleroma != null;
}
public boolean isPixelfed() {
return version.contains("compatible; Pixelfed");
}
public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata)

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel;
@@ -16,7 +17,6 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public Instant scheduledAt;
@RequiredField
public Params params;
@RequiredField
public List<Attachment> mediaAttachments;
@Override
@@ -24,8 +24,17 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
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
public static class Params {
public static class Params extends BaseModel {
@RequiredField
public String text;
public String spoilerText;
@@ -40,10 +49,16 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
public String applicationId;
public List<String> mediaIds;
public ContentType contentType;
@Override
public void postprocess() throws ObjectValidationException {
super.postprocess();
if (poll != null) poll.postprocess();
}
}
@Parcel
public static class ScheduledPoll {
public static class ScheduledPoll extends BaseModel {
@RequiredField
public String expiresIn;
@RequiredField

View File

@@ -12,6 +12,7 @@ import com.google.gson.JsonParseException;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.parceler.Parcel;
@@ -20,8 +21,6 @@ import java.lang.reflect.Type;
import java.time.Instant;
import java.util.List;
import androidx.annotation.NonNull;
@Parcel
public class Status extends BaseModel implements DisplayItemsParent, Searchable{
@RequiredField
@@ -39,7 +38,6 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
public boolean sensitive;
@RequiredField
public String spoilerText;
@RequiredField
public List<Attachment> mediaAttachments;
public Application application;
@RequiredField
@@ -96,6 +94,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
t.postprocess();
for(Emoji e:emojis)
e.postprocess();
if (mediaAttachments == null) mediaAttachments = List.of();
for(Attachment a:mediaAttachments)
a.postprocess();
account.postprocess();
@@ -173,6 +172,12 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
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) {
Status s = new Status();
s.id = id;

View File

@@ -14,7 +14,7 @@ public enum StatusPrivacy{
@SerializedName("local")
LOCAL(4); // akkoma
private int privacy;
private final int privacy;
StatusPrivacy(int privacy) {
this.privacy = privacy;
@@ -24,6 +24,13 @@ public enum StatusPrivacy{
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() {
return privacy;
}

View File

@@ -75,8 +75,7 @@ public class AccountSwitcherSheet extends BottomSheet{
this.fragment=fragment;
this.externalShare = externalShare;
this.openInApp = openInApp;
this.onClick = onClick;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity);

View File

@@ -12,151 +12,164 @@ import androidx.annotation.NonNull;
public class PhotoLayoutHelper{
public static final int MAX_WIDTH=1000;
public static final int MAX_HEIGHT=1910;
public static final int MAX_HEIGHT=1400;
public static final int MIN_HEIGHT=250;
public static final float GAP=1.5f;
@NonNull
public static TiledLayoutResult processThumbs(List<Attachment> thumbs){
int _maxW=MAX_WIDTH;
int _maxH=MAX_HEIGHT;
float maxRatio=MAX_WIDTH/(float)MAX_HEIGHT;
TiledLayoutResult result=new TiledLayoutResult();
if(thumbs.size()==1){
Attachment att=thumbs.get(0);
result.rowSizes=result.columnSizes=new int[]{1};
if(att.getWidth()>att.getHeight()){
result.width=_maxW;
result.height=Math.round(att.getHeight()/(float)att.getWidth()*_maxW);
float ratio=att.getWidth()/(float) att.getHeight();
if(ratio>maxRatio){
result.width=MAX_WIDTH;
result.height=Math.max(MIN_HEIGHT, Math.round(att.getHeight()/(float)att.getWidth()*MAX_WIDTH));
}else{
result.height=_maxH;
result.width=Math.round(att.getWidth()/(float)att.getHeight()*_maxH);
result.height=MAX_HEIGHT;
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){
throw new IllegalArgumentException("Empty thumbs array");
}
String orients="";
ArrayList<Float> ratios=new ArrayList<Float>();
ArrayList<Float> ratios=new ArrayList<>();
int cnt=thumbs.size();
boolean allAreWide=true, allAreSquare=true;
for(Attachment thumb : thumbs){
// float ratio=thumb.isSizeKnown() ? thumb.getWidth()/(float) thumb.getHeight() : 1f;
float ratio=thumb.getWidth()/(float) thumb.getHeight();
char orient=ratio>1.2 ? 'w' : (ratio<0.8 ? 'n' : 'q');
orients+=orient;
float ratio=Math.max(0.45f, thumb.getWidth()/(float) thumb.getHeight());
if(ratio<=1.2f){
allAreWide=false;
if(ratio<0.8f)
allAreSquare=false;
}else{
allAreSquare=false;
}
ratios.add(ratio);
}
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(orients.equals("ww") && 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));
if(allAreWide && avgRatio>1.4*maxRatio && (ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
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.height=Math.round(h*2+marginH);
result.width=MAX_WIDTH;
result.height=Math.round(h*2+GAP);
result.columnSizes=new int[]{result.width};
result.rowSizes=new int[]{Math.round(h), Math.round(h)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, maxW, h, 0, 0),
new TiledLayoutResult.Tile(1, 1, maxW, h, 0, 1)
new TiledLayoutResult.Tile(1, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 0, 1)
};
}else if(orients.equals("ww") || orients.equals("qq")){ // next to each other, same ratio
float w=((maxW-marginW)/2);
float h=Math.min(w/ratios.get(0), Math.min(w/ratios.get(1), maxH));
}else if(allAreWide || allAreSquare){ // next to each other, same ratio
float w=((MAX_WIDTH-GAP)/2);
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.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.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, w, h, 0, 0),
new TiledLayoutResult.Tile(1, 1, w, h, 1, 0)
new TiledLayoutResult.Tile(1, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 1, 0)
};
}else{ // next to each other, different ratios
float w0=((maxW-marginW)/ratios.get(1)/(1/ratios.get(0)+1/ratios.get(1)));
float w1=(maxW-w0-marginW);
float h=Math.min(maxH, Math.min(w0/ratios.get(0), w1/ratios.get(1)));
float w0=((MAX_WIDTH-GAP)/ratios.get(1)/(1/ratios.get(0)+1/ratios.get(1)));
float w1=(MAX_WIDTH-w0-GAP);
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.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.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, w0, h, 0, 0),
new TiledLayoutResult.Tile(1, 1, w1, h, 1, 0)
new TiledLayoutResult.Tile(1, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 1, 0)
};
}
}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
float hCover=Math.min(maxW/ratios.get(0), (maxH-marginH)*0.66f);
float w2=((maxW-marginW)/2);
float h=Math.min(maxH-hCover-marginH, Math.min(w2/ratios.get(1), w2/ratios.get(2)));
result.width=Math.round(maxW);
result.height=Math.round(hCover+h+marginH);
result.columnSizes=new int[]{Math.round(w2), _maxW-Math.round(w2)};
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(MAX_WIDTH/ratios.get(0), (MAX_HEIGHT-GAP)*0.66f);
float w2=((MAX_WIDTH-GAP)/2);
float h=Math.min(MAX_HEIGHT-hCover-GAP, Math.min(w2/ratios.get(1), w2/ratios.get(2)));
if(hCover+h<MIN_HEIGHT){
float prevTotalHeight=hCover+h;
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.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(2, 1, maxW, hCover, 0, 0),
new TiledLayoutResult.Tile(1, 1, w2, h, 0, 1),
new TiledLayoutResult.Tile(1, 1, w2, h, 1, 1)
new TiledLayoutResult.Tile(2, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 0, 1),
new TiledLayoutResult.Tile(1, 1, 1, 1)
};
}else{ // 2nd and 3rd photos are on the right part
float wCover=Math.min(maxH*ratios.get(0), (maxW-marginW)*0.75f);
float h1=(ratios.get(1)*(maxH-marginH)/(ratios.get(2)+ratios.get(1)));
float h0=(maxH-h1-marginH);
float w=Math.min(maxW-wCover-marginW, Math.min(h1*ratios.get(2), h0*ratios.get(1)));
result.width=Math.round(wCover+w+marginW);
result.height=Math.round(maxH);
float height=Math.min(MAX_HEIGHT, MAX_WIDTH*0.66f/avgRatio);
float wCover=Math.min(height*ratios.get(0), (MAX_WIDTH-GAP)*0.66f);
float h1=(ratios.get(1)*(height-GAP)/(ratios.get(2)+ratios.get(1)));
float h0=(height-h1-GAP);
float w=Math.min(MAX_WIDTH-wCover-GAP, Math.min(h1*ratios.get(2), h0*ratios.get(1)));
result.width=Math.round(wCover+w+GAP);
result.height=Math.round(height);
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
result.rowSizes=new int[]{Math.round(h0), Math.round(h1)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 2, wCover, maxH, 0, 0),
new TiledLayoutResult.Tile(1, 1, w, h0, 1, 0),
new TiledLayoutResult.Tile(1, 1, w, h1, 1, 1)
new TiledLayoutResult.Tile(1, 2, 0, 0),
new TiledLayoutResult.Tile(1, 1, 1, 0),
new TiledLayoutResult.Tile(1, 1, 1, 1)
};
}
}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
float hCover=Math.min(maxW/ratios.get(0), (maxH-marginH)*0.66f);
float h=(maxW-2*marginW)/(ratios.get(1)+ratios.get(2)+ratios.get(3));
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(MAX_WIDTH/ratios.get(0), (MAX_HEIGHT-GAP)*0.66f);
float h=(MAX_WIDTH-2*GAP)/(ratios.get(1)+ratios.get(2)+ratios.get(3));
float w0=h*ratios.get(1);
float w1=h*ratios.get(2);
float w2=h*ratios.get(3);
h=Math.min(maxH-hCover-marginH, h);
result.width=Math.round(maxW);
result.height=Math.round(hCover+h+marginH);
result.columnSizes=new int[]{Math.round(w0), Math.round(w1), _maxW-Math.round(w0)-Math.round(w1)};
h=Math.min(MAX_HEIGHT-hCover-GAP, h);
if(hCover+h<MIN_HEIGHT){
float prevTotalHeight=hCover+h;
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.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(3, 1, maxW, hCover, 0, 0),
new TiledLayoutResult.Tile(1, 1, w0, h, 0, 1),
new TiledLayoutResult.Tile(1, 1, w1, h, 1, 1),
new TiledLayoutResult.Tile(1, 1, w2, h, 2, 1),
new TiledLayoutResult.Tile(3, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 0, 1),
new TiledLayoutResult.Tile(1, 1, 1, 1),
new TiledLayoutResult.Tile(1, 1, 2, 1),
};
}else{ // 2nd, 3rd and 4th photos are on the right part
float wCover= Math.min(maxH*ratios.get(0), (maxW-marginW)*0.66f);
float w=(maxH-2*marginH)/(1/ratios.get(1)+1/ratios.get(2)+1/ratios.get(3));
float height=Math.min(MAX_HEIGHT, MAX_WIDTH*0.66f/avgRatio);
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 h1=w/ratios.get(2);
float h2=w/ratios.get(3)+marginH;
w=Math.min(maxW-wCover-marginW, w);
result.width=Math.round(wCover+marginW+w);
result.height=Math.round(maxH);
float h2=w/ratios.get(3)+GAP;
w=Math.min(MAX_WIDTH-wCover-GAP, w);
result.width=Math.round(wCover+GAP+w);
result.height=Math.round(height);
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
result.rowSizes=new int[]{Math.round(h0), Math.round(h1), Math.round(h2)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 3, wCover, maxH, 0, 0),
new TiledLayoutResult.Tile(1, 1, w, h0, 1, 0),
new TiledLayoutResult.Tile(1, 1, w, h1, 1, 1),
new TiledLayoutResult.Tile(1, 1, w, h2, 1, 2),
new TiledLayoutResult.Tile(1, 3, 0, 0),
new TiledLayoutResult.Tile(1, 1, 1, 0),
new TiledLayoutResult.Tile(1, 1, 1, 1),
new TiledLayoutResult.Tile(1, 1, 1, 2),
};
}
}else{
@@ -174,14 +187,14 @@ public class PhotoLayoutHelper{
HashMap<int[], float[]> tries=new HashMap<>();
// One line
int firstLine, secondLine, thirdLine;
tries.put(new int[]{firstLine=cnt}, new float[]{calculateMultiThumbsHeight(ratiosCropped, maxW, marginW)});
int firstLine, secondLine;
tries.put(new int[]{cnt}, new float[]{calculateMultiThumbsHeight(ratiosCropped, MAX_WIDTH, GAP)});
// Two lines
for(firstLine=1; firstLine<=cnt-1; firstLine++){
tries.put(new int[]{firstLine, secondLine=cnt-firstLine}, new float[]{
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), maxW, marginW),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, ratiosCropped.size()), maxW, marginW)
tries.put(new int[]{firstLine, cnt-firstLine}, new float[]{
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), MAX_WIDTH, GAP),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, ratiosCropped.size()), MAX_WIDTH, GAP)
}
);
}
@@ -189,23 +202,24 @@ public class PhotoLayoutHelper{
// Three lines
for(firstLine=1; firstLine<=cnt-2; firstLine++){
for(secondLine=1; secondLine<=cnt-firstLine-1; secondLine++){
tries.put(new int[]{firstLine, secondLine, thirdLine=cnt-firstLine-secondLine}, new float[]{
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), maxW, marginW),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, firstLine+secondLine), maxW, marginW),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine+secondLine, ratiosCropped.size()), maxW, marginW)
tries.put(new int[]{firstLine, secondLine, cnt-firstLine-secondLine}, new float[]{
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), MAX_WIDTH, GAP),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, firstLine+secondLine), MAX_WIDTH, GAP),
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;
float optDiff=0;
for(int[] conf : tries.keySet()){
float[] heights=tries.get(conf);
float confH=marginH*(heights.length-1);
float confH=GAP*(heights.length-1);
for(float h : heights) confH+=h;
float confDiff=Math.abs(confH-maxH);
float confDiff=Math.abs(confH-realMaxHeight);
if(conf.length>1){
if(conf[0]>conf[1] || conf.length>2 && conf[1]>conf[2]){
confDiff*=1.1;
@@ -222,7 +236,7 @@ public class PhotoLayoutHelper{
float[] optHeights=tries.get(optConf);
int k=0;
result.width=Math.round(maxW);
result.width=MAX_WIDTH;
result.rowSizes=new int[optHeights.length];
result.tiles=new TiledLayoutResult.Tile[thumbs.size()];
float totalHeight=0f;
@@ -240,11 +254,11 @@ public class PhotoLayoutHelper{
ArrayList<TiledLayoutResult.Tile> row=new ArrayList<>();
for(int j=0; j<lineThumbs.size(); j++){
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);
if(j<lineThumbs.size()-1 && !gridLineOffsets.contains(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;
row.add(tile);
k++;
@@ -252,7 +266,7 @@ public class PhotoLayoutHelper{
rowTiles.add(row);
}
Collections.sort(gridLineOffsets);
gridLineOffsets.add(Math.round(maxW));
gridLineOffsets.add(Math.round(MAX_WIDTH));
result.columnSizes=new int[gridLineOffsets.size()];
result.columnSizes[0]=gridLineOffsets.get(0);
for(int i=gridLineOffsets.size()-1; i>0; i--){
@@ -276,7 +290,7 @@ public class PhotoLayoutHelper{
columnOffset+=tile.colSpan;
}
}
result.height=Math.round(totalHeight+marginH*(optHeights.length-1));
result.height=Math.round(totalHeight+GAP*(optHeights.length-1));
}
return result;
@@ -310,19 +324,19 @@ public class PhotoLayoutHelper{
}
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.rowSpan=rowSpan;
this.width=width;
this.height=height;
this.startCol=startCol;
this.startRow=startRow;
}
public Tile(int colSpan, int rowSpan, float width, float height, int startCol, int startRow){
this(colSpan, rowSpan, Math.round(width), Math.round(height), startCol, startRow);
public Tile(int colSpan, int rowSpan, int startCol, int startRow, int width){
this(colSpan, rowSpan, startCol, startRow);
this.width=width;
}
@Override
@@ -330,8 +344,8 @@ public class PhotoLayoutHelper{
return "Tile{"+
"colSpan="+colSpan+
", rowSpan="+rowSpan+
", width="+width+
", height="+height+
", startCol="+startCol+
", startRow="+startRow+
'}';
}
}

View File

@@ -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)));
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)));
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);
if(item.notification.type == Notification.Type.FOLLOW_REQUEST && (relationship == null || !relationship.followedBy)){
actionWrap.setVisibility(View.GONE);

View File

@@ -33,12 +33,14 @@ import me.grishka.appkit.Nav;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
public final String accountID;
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);
this.status=status;
this.accountID=accountID;
}
@Override
@@ -72,7 +74,9 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public void onBind(ExtendedFooterStatusDisplayItem item){
Status s=item.status;
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){
editHistory.setVisibility(View.VISIBLE);
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));

View File

@@ -133,8 +133,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
boost.setEnabled(item.status.isReblogPermitted(item.accountID));
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
@@ -173,9 +172,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
} else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false;
touchingView = v;
// 28dp to center in middle of icon, because:
// (icon width = 24dp) / 2 + (paddingStart = 8dp) + (paddingHorizontal = 8dp)
v.setPivotX(UiUtils.sp(v.getContext(), 28));
v.setPivotX(V.sp(28));
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
if (disabled) return true;
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());

View File

@@ -196,7 +196,14 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
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);
}else if(item.scheduledStatus!=null){
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();
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

View File

@@ -10,6 +10,7 @@ import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
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.photoviewer.PhotoViewerHost;
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.MaxWidthFrameLayout;
import org.joinmastodon.android.ui.views.MediaGridLayout;
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 ArrayList<MediaAttachmentViewController> controllers=new ArrayList<>();
private final MaxWidthFrameLayout overlays;
private final FrameLayout altTextWrapper;
private final TextView altTextButton;
private final ImageView noAltTextButton;
@@ -105,8 +109,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
wrapper=(FrameLayout)itemView;
layout=new MediaGridLayout(activity);
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);
altTextButton=findViewById(R.id.alt_button);
noAltTextButton=findViewById(R.id.no_alt_button);
@@ -215,7 +224,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
int[] loc={0, 0};
v.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
wrapper.getLocationInWindow(loc);
overlays.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];
@@ -278,7 +287,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
int[] loc={0, 0};
btn.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
wrapper.getLocationInWindow(loc);
overlays.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];

View File

@@ -105,6 +105,21 @@ public abstract class StatusDisplayItem{
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){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
@@ -120,17 +135,7 @@ public abstract class StatusDisplayItem{
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
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);
replyLine = new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
);
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
}
if(status.reblog!=null){

View File

@@ -1,14 +1,10 @@
package org.joinmastodon.android.ui.text;
import android.graphics.Typeface;
import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontStyle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
@@ -17,13 +13,10 @@ import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import android.util.TypedValue;
import android.widget.TextView;
import 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.Hashtag;
import org.joinmastodon.android.model.Mention;
@@ -251,6 +244,10 @@ public class HtmlParser{
return Jsoup.clean(html, Safelist.none());
}
public static String text(String html) {
return Jsoup.parse(html).body().wholeText();
}
public static CharSequence parseLinks(String text){
Matcher matcher=URL_PATTERN.matcher(text);
if(!matcher.find()) // Return the original string if there are no URLs

View File

@@ -56,6 +56,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
@@ -1052,50 +1053,46 @@ public class UiUtils {
}, null);
}
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> resultConsumer) {
return lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results ->
!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) {
lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
public static Optional<MastodonAPIRequest<SearchResults>> lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer<Account> resultConsumer) {
return lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results ->
!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('_')))) {
resultConsumer.accept(query);
return;
return Optional.empty();
}
new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
return Optional.of(new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Optional<T> result = extractResult.apply(results);
if (result.isPresent()) resultConsumer.accept(result.get());
else {
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
resultConsumer.accept(null);
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
.exec(targetAccountID));
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
public static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else {
@@ -1110,11 +1107,35 @@ public class UiUtils {
}
}
private static Bundle bundleError(String error) {
Bundle args = new Bundle();
args.putString("error", error);
return args;
}
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, launchBrowser, (clazz, args) -> {
if (clazz == null) return;
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) {
@@ -1137,15 +1158,17 @@ public class UiUtils {
}
}
public static void lookupAccountHandle(Context context, String accountID, String query, BiConsumer<Class<? extends Fragment>, Bundle> go) {
parseFediverseHandle(query).ifPresentOrElse(
handle -> lookupAccountHandle(context, accountID, handle, go),
() -> go.accept(null, null)
);
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 void lookupAccountHandle(Context context, String accountID, Pair<String, Optional<String>> queryHandle, BiConsumer<Class<? extends Fragment>, Bundle> go) {
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(""));
new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
return new GetSearchResults(fullHandle, GetSearchResults.Type.ACCOUNTS, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1159,29 +1182,22 @@ public class UiUtils {
go.accept(ProfileFragment.class, args);
return;
}
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
args.putString("error", context.getString(R.string.sk_resource_not_found));
go.accept(null, null);
go.accept(null, bundleError(context.getString(R.string.sk_resource_not_found)));
}
@Override
public void onError(ErrorResponse error) {
Bundle args = new Bundle();
if (error instanceof MastodonErrorResponse e) {
args.putString("error", e.error);
args.putInt("httpStatus", e.httpStatus);
}
go.accept(null, args);
go.accept(null, bundleError(error));
}
}).exec(accountID);
}
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, BiConsumer<Class<? extends Fragment>, Bundle> go) {
public static Optional<MastodonAPIRequest<?>> lookupURL(Context context, String accountID, String url, BiConsumer<Class<? extends Fragment>, Bundle> go) {
Uri uri = Uri.parse(url);
List<String> path = uri.getPathSegments();
if (accountID != null && "https".equals(uri.getScheme())) {
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
new GetStatusByID(path.get(1))
return Optional.of(new GetStatusByID(path.get(1))
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result) {
@@ -1193,17 +1209,12 @@ public class UiUtils {
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
.exec(accountID));
} else if (looksLikeFediverseUrl(url)) {
new GetSearchResults(url, null, true)
return Optional.of(new GetSearchResults(url, null, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
@@ -1221,26 +1232,19 @@ public class UiUtils {
go.accept(ProfileFragment.class, args);
return;
}
if (launchBrowser) launchWebBrowser(context, url);
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(null, null);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
go.accept(null, bundleError(error));
}
})
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
.exec(accountID));
}
}
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
return Optional.empty();
}
public static void copyText(View v, String text) {
@@ -1420,16 +1424,6 @@ public class UiUtils {
}
}
/**
* Scale the input value according to the device's scaled display density
* @param sp Input value in scale-independent pixels (sp)
* @return Scaled value in physical pixels (px)
*/
public static int sp(Context context, float sp){
// TODO: replace with V.sp in next AppKit version
return Math.round(sp*context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity);
}
/**
* Wraps a View.OnClickListener to filter multiple clicks in succession.
* Useful for buttons that perform some action that changes their state asynchronously.

View File

@@ -13,7 +13,7 @@ import me.grishka.appkit.utils.V;
public class MediaGridLayout extends ViewGroup{
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 int[] columnStarts=new int[10], columnEnds=new int[10], rowStarts=new int[10], rowEnds=new int[10];
@@ -37,6 +37,9 @@ public class MediaGridLayout extends ViewGroup{
}
int width=Math.min(UiUtils.MAX_WIDTH, MeasureSpec.getSize(widthMeasureSpec));
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;
for(int i=0;i<tiledLayout.columnSizes.length;i++){
@@ -73,9 +76,13 @@ public class MediaGridLayout extends ViewGroup{
if(tiledLayout==null)
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;
if(r-l>UiUtils.MAX_WIDTH){
xOffset=(r-l)/2-UiUtils.MAX_WIDTH/2;
if(r-l>maxWidth){
xOffset=(r-l)/2-maxWidth/2;
}
for(int i=0;i<getChildCount();i++){

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -6,79 +6,79 @@
android:paddingEnd="4dp"
android:paddingStart="16dp">
<ImageView
android:id="@+id/more"
android:layout_width="36dp"
android:layout_height="36dp"
<LinearLayout
android:id="@+id/buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="13dp"
android:layout_alignParentTop="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 -->
android:layout_alignParentEnd="true">
<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:id="@+id/unread_indicator"
android:layout_width="36dp"
android:layout_height="36dp"
android:visibility="gone"
android:tint="?android:colorAccent"
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" />
</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
android:id="@+id/avatar"
@@ -94,7 +94,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_toStartOf="@id/unread_indicator"
android:layout_toStartOf="@id/buttons"
android:layout_toEndOf="@id/avatar"
android:layout_above="@+id/username_wrap">
@@ -127,7 +127,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_toStartOf="@id/unread_indicator"
android:layout_toStartOf="@id/buttons"
android:layout_toEndOf="@id/avatar"
android:layout_alignBottom="@id/avatar"
android:layoutDirection="locale"

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -2,7 +2,16 @@
<resources>
<string name="sk_confirm_delete_and_redraft">هل أنت متأكد أنك تريد حذف وإعادة صياغة هذا المنشور؟</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_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>

View File

@@ -101,7 +101,7 @@
<string name="sk_already_reblogged">Bereits geteilt</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_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_draft">Entwurf</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_server_version">Server-Version: %s</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_expand">Erweitern</string>
<string name="sk_collapse">Einklappen</string>
@@ -294,6 +294,14 @@
<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 auf deiner Heim-Instanz nicht laden.</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>

View File

@@ -245,9 +245,9 @@
<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_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_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_notify_poll_results">Resultado de encuestas</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_unfinished_attachments">¿Corregir adjuntos\?</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_settings_hide_interaction">Ocultar los botones de interacción</string>
<string name="sk_follow_as">Seguir desde otra cuenta</string>
@@ -286,10 +286,23 @@
<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_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 gente en tu servidor de Akkoma.</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>

View File

@@ -12,51 +12,85 @@
<string name="in_reply_to">در پاسخ به %s</string>
<string name="notifications">اعلان‌ها</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="notification_boosted">فرستهٔ شما را تقویت کرد</string>
<string name="poll_ended">نظرسنجی به پایان رسید</string>
<string name="time_seconds">%dثانیه</string>
<string name="time_minutes">%dدقیقه</string>
<string name="time_hours">%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="publish">انتشار</string>
<string name="discard_draft">پیش‌نویس‌ کنار گذاشته شود؟</string>
<string name="discard">صرف‌نظر کردن</string>
<string name="cancel">لغو</string>
<string name="posts">پست‌ها</string>
<string name="posts_and_replies">پست‌ها و پاسخ‌ها</string>
<plurals name="followers">
<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="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="mute_user">بی‌صدا %s</string>
<string name="unmute_user">ناخموشی %s</string>
<string name="block_user">مسدود %s</string>
<string name="unblock_user">رفع مسدودیت %s</string>
<string name="report_user">گزارش کردن %s</string>
<string name="profile_joined">عضو شد</string>
<string name="done">انجام شد</string>
<string name="loading">درحال بارگذاری…</string>
<string name="field_label">برچسب</string>
<string name="field_content">محتوا</string>
<string name="saving">درحال ذخیره‌سازی…</string>
<string name="poll_closed">پایان‌یافته</string>
<string name="confirm_mute_title">خموشی حساب</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="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="delete">حذف</string>
<string name="confirm_delete_title">حذف پست</string>
<string name="confirm_delete_title">حذف فرسته</string>
<string name="play">پخش</string>
<string name="pause">توقف</string>
<string name="log_out">خروج</string>
<string name="add_account">افزودن حساب</string>
<string name="search_hint">جستجو</string>
<string name="hashtags">هشتگها</string>
<string name="hashtags">برچسبها</string>
<string name="news">اخبار</string>
<string name="for_you">برای شما</string>
<string name="all_notifications">همه</string>
<string name="report_title">گزارش کردن %s</string>
<string name="report_reason_personal">من این را دوست ندارم</string>
<string name="report_reason_spam">این هرزنامه است</string>
<string name="sending_report">درحال ارسال گزارش…</string>
<string name="unfollow">پی‌نگرفتن</string>
<string name="back">بازگشت</string>
<string name="instance_rules_title">قوانین کارساز</string>
<string name="signup_title">ایجاد حساب</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_all">همه</string>
<string name="category_art">هنر</string>
@@ -68,34 +102,118 @@
<string name="category_tech">فناوری</string>
<!-- %s is the email address -->
<string name="resend">ارسال دوباره</string>
<string name="retry_upload">بارگذاری مجدد</string>
<string name="edit_image">ویرایش تصویر</string>
<string name="save">ذخیره</string>
<string name="visibility_public">عمومی</string>
<string name="visibility_followers_only">فقط پی‌گیرندگان</string>
<string name="search_all">همه</string>
<string name="search_people">افراد</string>
<string name="skip">بعدی</string>
<string name="notification_type_favorite">علاقه‌مندی‌ها</string>
<string name="notification_type_reblog">تقویت‌ها</string>
<string name="notification_type_poll">نظرسنجی‌ها</string>
<string name="choose_account">انتخاب حساب</string>
<string name="theme_auto">خودکار</string>
<string name="theme_light">روشن</string>
<string name="theme_dark">تاریک</string>
<string name="new_post">پست جدید</string>
<string name="new_post">فرسته جدید</string>
<string name="button_reply">پاسخ</string>
<string name="button_reblog">تقویت</string>
<string name="button_favorite">برگزیده‌</string>
<string name="button_share">اشتراک‌گذاری</string>
<string name="add_media">افزودن رسانه</string>
<string name="add_poll">افزودن نظرسنجی</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="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 -->
<plurals name="x_favorites">
<item quantity="one">%,d پسندیده</item>
<item quantity="other">%,d پسندیده‌ها</item>
</plurals>
<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>
<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="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 -->
<string name="update_available">ماستودون برای اندروید %s آماده بارگیری است.</string>
<!-- %s is version like 1.2.3 -->
<string name="update_ready">ماستودون برای اندروید %s بارگیری شده و آماده نصب است.</string>
<!-- %s is file size -->
<string name="download_update">بارگیری (%s)</string>
<string name="install_update">نصب</string>
<string name="privacy_policy_title">حریم خصوصی شما</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 -->
<string name="profile_bio">دربارهٔ شما</string>
<!-- 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. -->
<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>

View 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>

View File

@@ -254,7 +254,7 @@
<string name="sk_expand">Développer</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_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_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>
@@ -294,4 +294,16 @@
<string name="sk_timeline_bubble">Bulle</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>

View File

@@ -181,7 +181,7 @@
<string name="sk_settings_local_only_explanation">Vaša početna instanca mora podržavati isključivo-lokalno objavljivanje da bi ovo radilo. Većina modificiranih verzija Mastodona to radi, ali Mastodon ne.</string>
<string name="sk_settings_hide_interaction">Sakrij gumbe za interakciju</string>
<string name="sk_settings_hide_fab">Automatski sakrij gumb \"Nova objava\"</string>
<string name="sk_quoting_user">"Citiranje %"</string>
<string name="sk_quoting_user">Citiranje %s</string>
<string name="sk_settings_reply_visibility">Vidljivost odgovora</string>
<string name="sk_settings_reply_visibility_all">Svi odgovori</string>
<string name="sk_settings_reply_visibility_following">Odgovori onima koje pratim</string>

View File

@@ -10,7 +10,7 @@
<string name="ok">Oke</string>
<string name="preparing_auth">Menyiapkan untuk autentikasi…</string>
<string name="finishing_auth">Menyelesaikan autentikasi…</string>
<string name="user_boosted">%s di-boost-kan</string>
<string name="user_boosted">%s membagikan</string>
<string name="in_reply_to">Membalas ke %s</string>
<string name="notifications">Notifikasi</string>
<string name="user_followed_you">mengikuti Anda</string>
@@ -417,6 +417,7 @@
<string name="show">Tampilkan</string>
<string name="hide">Sembunyikan</string>
<string name="join_default_server">Gabung %s</string>
<string name="pick_server">Cari server lain</string>
<string name="signup_or_login">atau</string>
<string name="learn_more">Pelajari lebih lanjut</string>
<string name="welcome_to_mastodon">Selamat datang di Mastodon</string>

View File

@@ -289,8 +289,17 @@
<string name="sk_settings_content_types_explanation">Memperbolehkan menetapkan jenis konten seperti Markdown ketika membuat kiriman. Perlu diingat bahwa tidak semua server mendukung ini.</string>
<string name="sk_open_in_app">Buka dalam aplikasi</string>
<string name="sk_external_share_title">Bagikan dengan akun</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yamg paling terkini oleh orang-orang dalam gelembung server Akkoma Anda.</string>
<string name="sk_bubble_timeline_info_banner">Ini adalah kiriman yang paling terkini dari jaringan dikurasikan oleh admin server Anda.</string>
<string name="sk_timeline_bubble">Gelembung</string>
<string name="sk_instance_info_unavailable">Info server sementara tidak tersedia</string>
<string name="sk_external_share_or_open_title">Bagikan atau buka dengan akun</string>
<string name="sk_no_remote_info_hint">info jarak jauh tidak tersedia</string>
<string name="sk_settings_allow_remote_loading">Muat info dari server jarak jauh</string>
<string name="sk_settings_allow_remote_loading_explanation">Coba mendapatkan pendaftaran akurat untuk pengikut cr</string>
<string name="sk_error_loading_profile">Gagal memuat profil melalui %s</string>
<string name="sk_open_in_app_failed">Tidak dapat buka dalam aplikasi</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Tampilkan peringatan konten yang sama dari</string>
<string name="sk_settings_auto_reveal_nobody">bukan siapa pun</string>
<string name="sk_settings_auto_reveal_author">pembuat</string>
<string name="sk_settings_auto_reveal_anyone">semuanya</string>
</resources>

View File

@@ -274,4 +274,26 @@
<string name="sk_compact_reblog_reply_line">Linea boost/risposta compatta</string>
<string name="sk_reacted_with">ha reagito con %s</string>
<string name="sk_reply_line_above_avatar">Linea \"In risposta a\" sopra l\'avatar</string>
<string name="sk_bubble_timeline_info_banner">Questi sono i post più recenti della rete, curati dagli amministratori della tua istanza.</string>
<string name="sk_settings_content_types_explanation">Permette di impostare un tipo di contenuto, come Markdown, quando si crea un post. Tieni presente che non tutte le istanze lo supportano.</string>
<string name="sk_open_in_app_failed">Impossibile aprire nell\'app</string>
<string name="sk_external_share_title">Condividi con l\'account</string>
<string name="sk_external_share_or_open_title">Condividi o apri con l\'account</string>
<string name="sk_no_remote_info_hint">Informazioni remote non disponibili</string>
<string name="sk_error_loading_profile">Impossibile caricare il profilo tramite %s</string>
<string name="sk_settings_allow_remote_loading">Carica informazioni da istanze remote</string>
<string name="sk_settings_allow_remote_loading_explanation">Cerca di ottenere elenchi più accurati di follower, like e boost caricando le informazioni dall\'istanza di origine.</string>
<string name="sk_content_type">Tipo di contenuto</string>
<string name="sk_timeline_bubble">Bolla</string>
<string name="sk_content_type_unspecified">Non specificato</string>
<string name="sk_content_type_plain">Testo semplice</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_settings_content_types">Abilita la formattazione dei post</string>
<string name="sk_settings_default_content_type">Tipo di contenuto predefinito</string>
<string name="sk_settings_default_content_type_explanation">Ti permette di preselezionare un tipo di contenuto quando si creano nuovi post, sovrascrivendo il valore impostato in \"Preferenze di pubblicazione\".</string>
<string name="sk_instance_info_unavailable">Informazioni sull\'istanza temporaneamente non disponibili</string>
<string name="sk_open_in_app">Apri nell\'app</string>
</resources>

View File

@@ -254,4 +254,9 @@
<string name="sk_quoting_user">Quoting %s</string>
<string name="sk_settings_reply_visibility">Zichtbaarheid reactie</string>
<string name="sk_settings_reply_visibility_all">Alle reacties</string>
<string name="sk_settings_auto_reveal_anyone">iedereen</string>
<string name="sk_settings_auto_reveal_author">auteur</string>
<string name="sk_settings_auto_reveal_nobody">niemand</string>
<string name="sk_settings_prefix_replies_never">niemand</string>
<string name="sk_settings_prefix_replies_always">iedereen</string>
</resources>

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sk_app_name">Megalodon</string>
<string name="sk_pinned_posts">Fixado</string>
<string name="sk_delete_and_redraft">Apagar e reescrever</string>
<string name="sk_confirm_delete_and_redraft">Tem a certeza que pretende apagar e reescrever esta publicação\?</string>
<string name="sk_confirm_unpin_post">Tem a certeza que deseja desafixar esta publicação\?</string>
<string name="sk_settings_reply_visibility_all">Todas as respostas</string>
<string name="sk_settings_load_new_posts">Carregar novas publicações automaticamente</string>
<string name="sk_user_post_notifications_off">Desligar as notificações de publicação para %s</string>
<string name="sk_federated_timeline_info_banner">Estas são as publicações mais recentes das pessoas na tua federação.</string>
<string name="sk_confirm_delete_and_redraft_title">Apagar e reescrever publicação</string>
<string name="sk_pin_post">Fixar no perfil</string>
<string name="sk_confirm_pin_post_title">Fixar publicação no perfil</string>
<string name="sk_confirm_pin_post">Deseja fixar esta publicação ao seu perfil\?</string>
<string name="sk_pinning">A fixar a publicação…</string>
<string name="sk_unpin_post">Desafixar do perfil</string>
<string name="sk_confirm_unpin_post_title">Desafixar publicação do perfil</string>
<string name="sk_unpinning">A desafixar publicação…</string>
<string name="sk_image_description">Descrição da imagem</string>
<string name="sk_visibility_unlisted">Não listado</string>
<string name="sk_settings_show_replies">Mostrar respostas</string>
<string name="sk_quoting_user">Citação %s</string>
<string name="sk_settings_reply_visibility">Visibilidade da resposta</string>
<string name="sk_clear_recent_languages">Apagar idiomas usados recentemente</string>
<string name="sk_settings_reply_visibility_following">Respostas aos meus comentários</string>
<string name="sk_settings_reply_visibility_self">Respostas a mim</string>
<string name="sk_settings_show_boosts">Mostrar impulsionamentos</string>
<string name="sk_settings_show_interaction_counts">Mostrar contagem de interações</string>
<string name="sk_settings_app_version">Megalodon v%1$s (%2$d)</string>
<string name="sk_mark_media_as_sensitive">Marcar conteúdo como sensível</string>
<string name="sk_user_post_notifications_on">Ligar as notificações de publicação para %s</string>
<string name="sk_federated_timeline">Federação</string>
<string name="sk_update_available">Megalodon %s pronto a descarregar.</string>
<string name="sk_update_ready">Megalodon %s descarregado e pronto a instalar.</string>
<string name="sk_check_for_update">A verificar atualizações</string>
<string name="sk_no_update_available">Sem atualizações disponíveis</string>
<string name="sk_list_timelines">Listas</string>
<string name="sk_follow_requests">Pedidos para seguir</string>
<string name="sk_accept_follow_request">Aceitar pedido para seguir</string>
<string name="sk_reject_follow_request">Rejeitar pedido para seguir</string>
<string name="sk_lists_with_user">Listas com %s</string>
<string name="sk_settings_always_reveal_content_warnings">Mostrar sempre avisos de conteúdo</string>
</resources>

View File

@@ -257,7 +257,7 @@
<string name="sk_settings_collapse_long_posts">Сворачивать очень длинные посты</string>
<string name="sk_unfinished_attachments">Исправить вложения\?</string>
<string name="sk_unfinished_attachments_message">Некоторые вложения еще не загрузились.</string>
<string name="sk_quoting_user">Цитирование%s</string>
<string name="sk_quoting_user">Цитирование %s</string>
<string name="sk_settings_reply_visibility">Видимость ответа</string>
<string name="sk_settings_hide_interaction">Скрыть кнопки взаимодействия</string>
<string name="sk_follow_as">Подписаться с другого аккаунта</string>

View File

@@ -269,6 +269,7 @@
<string name="hide_content">Dölj innehåll</string>
<string name="new_post">Nytt inlägg</string>
<string name="button_reply">Svara</string>
<string name="button_reblog">Boosta</string>
<string name="button_favorite">Favoritmarkera</string>
<string name="button_share">Dela</string>
<string name="media_no_description">Media utan beskrivning</string>
@@ -428,5 +429,7 @@
<string name="signup_or_login">eller</string>
<string name="learn_more">Läs mer</string>
<string name="welcome_to_mastodon">Välkommen till Mastodon</string>
<string name="welcome_paragraph1">Mastodon är ett decentraliserat socialt nätverk, vilket innebär att inget enskilt företag kontrollerar det. Det består av många oberoende servrar, alla sammankopplade.</string>
<string name="what_are_servers">Vad är servrar?</string>
<string name="welcome_paragraph2"><![CDATA[Varje Mastodon-konto finns på en server — var och en med sina värderingar, regler och administratörer. Oavsett vilken du väljer kan du följa och interagera med människor på vilken server som helst.]]></string>
</resources>

View File

@@ -293,4 +293,8 @@
<string name="sk_instance_info_unavailable">Сервер тимчасово недоступний</string>
<string name="sk_external_share_or_open_title">Поділитися або відкрити за допомогою облікового запису</string>
<string name="sk_open_in_app_failed">Не вдалося відкрити в застосунку</string>
<string name="sk_no_remote_info_hint">віддалена інформація недоступна</string>
<string name="sk_settings_allow_remote_loading">Завантажити інформацію з віддалених серверів</string>
<string name="sk_settings_allow_remote_loading_explanation">Спробуйте отримати точніші списки підписників, вподобань і поширень, завантаживши інформацію з джерела.</string>
<string name="sk_error_loading_profile">Не вдалося завантажити профіль на ваш домашній сервер.</string>
</resources>

View File

@@ -260,7 +260,7 @@
<string name="sk_new_reports">New reports</string>
<string name="sk_settings_server_version">Server version: %s</string>
<string name="sk_notify_poll_results">Poll results</string>
<string name="sk_settings_prefix_reply_cw_with_re">Prefix reply CW with “re:”</string>
<string name="sk_settings_prefix_reply_cw_with_re">Prefix CW with “re:” on replies to</string>
<string name="sk_filtered">Filtered: %s</string>
<string name="sk_expand">Expand</string>
<string name="sk_collapse">Collapse</string>
@@ -294,7 +294,15 @@
<string name="sk_external_share_title">Share with account</string>
<string name="sk_external_share_or_open_title">Share or open with account</string>
<string name="sk_no_remote_info_hint">remote info unavailable</string>
<string name="sk_error_loading_profile">Failed loading the profile on your home instance.</string>
<string name="sk_error_loading_profile">Failed loading the profile via %s</string>
<string name="sk_settings_allow_remote_loading">Load info from remote instances</string>
<string name="sk_settings_allow_remote_loading_explanation">Try fetching more accurate listings for followers, likes and boosts by loading the information from the instance of origin.</string>
<string name="sk_settings_auto_reveal_equal_spoilers">Reveal same CWs in replies from</string>
<string name="sk_settings_auto_reveal_nobody">nobody</string>
<string name="sk_settings_auto_reveal_author">author</string>
<string name="sk_settings_auto_reveal_anyone">everyone</string>
<string name="sk_settings_prefix_replies_always">everyone</string>
<string name="sk_settings_prefix_replies_never">nobody</string>
<string name="sk_settings_prefix_replies_to_others">others</string>
<string name="sk_settings_forward_report_default">“Forward report” switch default</string>
</resources>

View File

@@ -2,4 +2,4 @@
- Verbesserte Kopfzeilen für Reblogs und Antworten in der Timeline
- Benachrichtigungs-Punkt (Benachrichtigungen werden aktuell noch nicht automatisch nachgeladen)
- Für Akkoma-Benutzer_innen: Antwort-Sichtbarkeit, sortierte Thread-Antworten, Zitate, …
- Crashes behoben und kleinere Verbesserungen
- Crashes behoben und kleinere Verbesserungen

View File

@@ -0,0 +1,8 @@
- Verbesserte, übersichtlichere Thread-Ansicht
- Website an Megalodon teilen, um den Account/Beitrag zu öffnen
- Laden von Follower*innen/Favorisierungen/… von der Ursprungs-Instanz
- Verbesserte Kompatibilität mit Nicht-Mastodon-Servern
- Option, um Inhaltstyp eines Beitrags festzulegen
- URL kopieren in der Recent-Apps-Ansicht auf Pixel-Geräten
- Automatisches Aufdecken von gleichen CWs in Threads
- Fehlerbehebungen und UI-Verbesserungen

View File

@@ -0,0 +1,8 @@
- Improved, clearer thread view
- Open accounts/posts by sharing a website to Megalodon
- Load followers/favorites/… listings from origin instance
- Compatibility improvements for non-Mastodon servers
- Option to set post content type
- Support for copying URL from Recent apps on Pixel devices
- Auto-reveal equal CWs in threads
- Bugfixes and UI improvements

View File

@@ -0,0 +1,8 @@
- Visualización de hilos mejorada y más clara
- Abre cuentas/publicaciones compartiendo una web con Megalodon
- Carga seguidores, favoritos etc. desde la instancia de origen
- Mejoras de compatibilidad con servidores no-Mastodon
- Opción para configurar el tipo de contenido en las publicaciones
- Soporte para copiar una URL desde la vista de Apps recientes en dispositivos Pixel
- Se muestran los Avisos de Contenido iguales automáticamente en hilos
- Corrección de errores y mejoras en la interfaz