Compare commits

..

42 Commits

Author SHA1 Message Date
LucasGGamerM
e15dd6024f Bumped version number 2022-12-07 13:28:07 -03:00
LucasGGamerM
e52dffeece Fix notification logo and lets start splash screen button color 2022-12-07 12:46:26 -03:00
LucasGGamerM
5b85bb427d bumping version code 2022-12-07 09:50:03 -03:00
LucasGGamerM
4d62388617 Fixing the notification icon once again... 2022-12-07 09:36:50 -03:00
LucasGGamerM
04b8055474 Fixing the notification icon once more 2022-12-07 09:22:18 -03:00
LucasGGamerM
3c34b6a7d2 Upping the version code once more, and fixing the self updater 2022-12-06 22:06:49 -03:00
LucasGGamerM
de4964c2cd Upping the version code and changing notification icon. This should be the first release 2022-12-06 21:58:35 -03:00
LucasGGamerM
fbcaa05c03 Changing the name of the archivesBaseName 2022-12-06 21:49:46 -03:00
LucasGGamerM
883f28696e Editing a while lot of files. New icon, new notification icon, a bunch of new stuff! 2022-12-06 21:21:43 -03:00
LucasGGamerM
df52230837 Editing the readme just in case 2022-12-06 19:23:28 -03:00
LucasGGamerM
a90f26a37a Setting update client 2022-12-06 19:20:47 -03:00
LucasGGamerM
8c1f76d7fa Initial Moshidon "release" 2022-12-06 19:17:07 -03:00
LucasGGamerM
f384d44f8f Changing my app id 2022-12-06 13:03:26 -03:00
LucasGGamerM
4ab6ed55f5 Changing my version number and string 2022-12-06 12:41:03 -03:00
LucasGGamerM
cf99bf5152 Merge branch 'proper_implementation_of_the_color_picker'
Just fixing stuff here and there
2022-12-05 17:47:03 -03:00
LucasGGamerM
10779717cf Make follow requests icon badge follow the color scheme and also make it that the profile top bar menu also follows the theme. This should be it 2022-12-05 17:20:40 -03:00
LucasGGamerM
db4c1bfe47 Merge branch 'proper_implementation_of_the_color_picker'
Just making it tidy and better :D
2022-12-05 14:11:20 -03:00
LucasGGamerM
27afba1cf2 Making it so that the boost icon is also following the theme when clicked 2022-12-05 14:10:34 -03:00
LucasGGamerM
4895425b40 Adding a proper logo to the top of the home timeline 2022-12-05 13:27:09 -03:00
LucasGGamerM
004c414fba Editing some drawable files to make them also follow the theme 2022-12-05 09:41:56 -03:00
LucasGGamerM
c8e38b134c Fixing weird bug with the buttons for the second time 2022-12-05 09:03:48 -03:00
LucasGGamerM
de5a911286 Fixing weird bug with the buttons 2022-12-04 22:52:27 -03:00
LucasGGamerM
606cd7442e Make it so that the publish button also follows the theme 2022-12-04 14:45:42 -03:00
LucasGGamerM
3ebc972268 Fixing the TrueBlack themes for everything 2022-12-04 14:00:10 -03:00
LucasGGamerM
4e39bb381c Making it so that the fab follows the theme 2022-12-04 13:14:38 -03:00
LucasGGamerM
b6178681b0 Adding yellow theme 2022-12-04 11:42:41 -03:00
LucasGGamerM
29abf70cec Adding orange theme, tweaking the blue and green theme 2022-12-04 11:16:58 -03:00
LucasGGamerM
8d63be513d Fix readability issue on the light blue theme 2022-12-04 10:24:51 -03:00
LucasGGamerM
e63b9d0dd6 Adding an icon to the color picker setting 2022-12-03 22:29:41 -03:00
LucasGGamerM
b1fda17ac7 Make badged settings icon follow accent colors 2022-12-03 16:48:12 -03:00
LucasGGamerM
bad44b145c Adding blue theme and refactoring styles.xml 2022-12-03 16:25:28 -03:00
LucasGGamerM
77669cedf6 More polishes over the green theme 2022-12-03 13:44:40 -03:00
LucasGGamerM
19238c389f Making the green theme more readable 2022-12-03 12:29:51 -03:00
LucasGGamerM
1747ff98b5 Adding a green theme 2022-12-02 14:00:58 -03:00
LucasGGamerM
8fa5824e3e Disabling the icons for the color picker menu 2022-12-02 11:58:40 -03:00
LucasGGamerM
6a674d7a7e Polishes 2022-12-01 19:55:53 -03:00
LucasGGamerM
dad3b8cd6b Proper implementation on the color picker. 2022-12-01 19:42:21 -03:00
LucasGGamerM
9179d2198d Fully fixed now, it should be ready to release. 2022-11-29 13:09:32 -03:00
LucasGGamerM
d096bef234 Fixing the fix of the bug I found. 2022-11-28 21:30:32 -03:00
LucasGGamerM
f2c47a1b84 Fixing another bug I found. 2022-11-28 21:24:00 -03:00
LucasGGamerM
bc2ac4e915 Fixed a few bugs from the earlier commit. 2022-11-28 16:47:04 -03:00
LucasGGamerM
ff215412c8 Adding mastodon original colors toggle. Partially fixing #90 2022-11-28 15:40:29 -03:00
525 changed files with 3261 additions and 11433 deletions

View File

@@ -1,32 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Screenshots and screen recordings**
If applicable, add screenshots (and screen recordings, if possible) to help explain your problem.
**Version**
Megalodon version: [e.g. v1.1.4+fork.#]
**Additional context**
- Does this issue also occur with the respective upstream release? (Please test using the respective `upstream-xxxxxx.apk` provided in [Releases](https://github.com/sk22/megalodon/releases)) No / Yes (`mastodon#…`)
> In this case, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead. If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue!
**Crash log**
If you know your way around Android development tools, please consider attaching a crash log, if possible.

View File

@@ -1,20 +0,0 @@
---
name: Feature/UI request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
If applicable: a clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,10 +0,0 @@
---
name: It's something else…
about: Issues that can't be categorized as feature requests or bug reports
title: ''
labels: ''
assignees: ''
---

106
README.md
View File

@@ -1,25 +1,23 @@
![Pink logo with pink shark](mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
# Megalodon
# Moshidon
[![Translation status](https://translate.codeberg.org/widgets/megalodon/-/svg-badge.svg)](https://translate.codeberg.org/engage/megalodon/)
 
[![Download latest release](https://img.shields.io/badge/dynamic/json?color=d92aad&label=Download%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fsk22%2Fmegalodon%2Freleases%2Flatest&style=flat)](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk)
> A fork of [megalodon](https://github.com/sk22/megalodon) which is a fork of [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, bookmarks and an image description viewer.
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
&nbsp;
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
**Warning! [The last version's integrated updater was broken](https://github.com/sk22/megalodon/issues/106) I already published a fixed version! If you're not updating through Izzy's F-Droid repository (more sources to come, hopefully!), you'll have to download the current release manually. Sorry about that!**
> 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.
[![Download latest release](https://img.shields.io/badge/dynamic/json?color=d92aad&label=download%20apk&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fsk22%2Fmastodon-android-fork%2Freleases%2Flatest&style=for-the-badge)](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk)
---
## 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”).**
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Local”, “Community” and “Posts”).**
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.
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 reposted/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).
@@ -31,12 +29,6 @@ Despite being one of the main features of federated social media, the Federated
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!
### Draft and schedule posts
**Allows for preparing a post and scheduling it to send it automatically at a specific time.**
You can create drafts, edit them, send them manually later or set a scheduled date. Drafts are technically saved as scheduled posts, so you can view and edit them from other apps that support scheduled posts. Scheduled posts are handled by your home instance, so they'll work even if you uninstall Megalodon.
### **Image description viewer**
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
@@ -49,40 +41,24 @@ This is important to **ensure the content youre sharing is as accessible as p
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.
### **Bookmarks**
**They allow for quickly saving posts and viewing them through the Bookmarks button on the top right of your profile.**
To bookmark a post, press the button between the Favorite and Share buttons on the bottom of the post. Bookmarks are saved privately, so the post authors wont know you saved their post the list of bookmarked posts is only visible to you.
## Installation
### IzzyOnDroid
**Press the download button above to download the APK. Open the downloaded file on your Android device to install it. Megalodon will automatically notify you about new updates inside the app.**
[apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk)
<a href="#installation"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
Note that you'll need to add Izzy's F-Droid repository to your F-Droid app first:
[`https://apt.izzysoft.de/fdroid/repo`](https://apt.izzysoft.de/fdroid/repo)
### Google Play Store
[play.google.com/store/apps/details?id=org.joinmastodon.android.sk](https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
### F-Droid
**[F-Droid.org?](https://f-droid.org)** Not yet, sorry!
If you want, you can help me figure out if something's missing in the [Issue #47: F-Droid.org](https://github.com/sk22/megalodon/issues/47)
### Direct
Press the download button to download the APK. Open the downloaded file on your Android device to install it. Megalodon will automatically notify you about new updates inside the app.
[![Download latest release](https://img.shields.io/badge/dynamic/json?color=d92aad&label=Download%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Fsk22%2Fmegalodon%2Freleases%2Flatest&style=flat)](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk)
You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/sk22/megalodon/releases) page.
To install this app on your Android device, download the [latest release from GitHub](https://github.com/sk22/megalodon/releases/latest/download/megalodon.apk) and open it. You might have to accept installing APK files from your browser when trying to install it. You can also take a look at all releases on the [Releases](https://github.com/sk22/megalodon/releases) page.
Megalodon makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)s automatic update checker. Megalodon will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
### Other sources
* **[Izzy's F-Droid repository](https://apt.izzysoft.de/fdroid/repo)**: https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.sk
---
@@ -102,19 +78,6 @@ This is an **unmodified version** of the official [Mastodon for Android](https:/
Variant without the integrated updater. This is the variant to be published to F-Droid.org where an integrated updater is not necessary. -->
---
## Contribution
### Translation
As with the source code, the translation is sourced from the official project, which you can contribute to on the official “**Mastodon for Android**” Crowdin project: https://crowdin.com/project/mastodon-for-android
There's also a handful of custom strings exclusive to this projects that would need to be translated. You can help translate **Megalodon** on Weblate: https://translate.codeberg.org/projects/megalodon/
[![Translation status](https://translate.codeberg.org/widgets/megalodon/-/horizontal-auto.svg)](https://translate.codeberg.org/engage/megalodon/)
---
@@ -131,7 +94,7 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Implement a bookmark button and list](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
* [Add “Check for update” button in addition to integrated update checker](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/check-for-update-button)
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/mark-media-as-sensitive)
* [Add settings to hide replies and reblogs from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
* [Follow and unfollow hashtags](https://github.com/sk22/megalodon/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
* [Notification bell for posts](https://github.com/sk22/megalodon/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
* [Viewing lists and adding/removing users from lists](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
@@ -141,14 +104,7 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-reply-visibility)
* [Clickable reply/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
* [Add push notification setting for post notifications](https://github.com/sk22/megalodon/commit/b190480d7739be47f23543d9e7644660f9b4b4ee)
* [Add option to allow voting for multiple options on polls](https://github.com/sk22/megalodon/commit/5b28468efd49387b4f8b83f142f3adf3104ca60c)
* [Add translate function](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/translate-button)
* [Add language selector](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/language-selector)
* [Implement deleting notifications](https://github.com/sk22/megalodon/commit/b0f9ce081f69f29ad59658fc00ca41372cd2677d) (disabled by default)
* [Long-click boost button to "quote" a post](https://github.com/sk22/megalodon/commit/b25a237c20c6a924ed4d9b357999867c3a32b32b)
* [Draft and schedule posts](https://github.com/sk22/megalodon/pull/217)
* [Display original post when replying](https://github.com/sk22/megalodon/commit/375f8ceb2747705fedf43686681cc0e0b812f899)
* [Clickable reply line while replying to open original post](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/clickable-reply-line-compose)
### Behavior
@@ -160,17 +116,6 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
* [No ellipsis for long poll answers](https://github.com/mastodon/mastodon-android/commit/c9aae828e2518adccdc092e41f8d1f0489636271)
* [Show poll vote button for multiple and single answer polls](https://github.com/mastodon/mastodon-android/commit/e14dfda2fdf32f0fa3043504ac5831683a87559a)
* [Show own vote after voting](https://github.com/mastodon/mastodon-android/commit/4ab9e25fec4fd9c10b7a8ddd1be522b3cc12cf28) ([Closes issue](https://github.com/mastodon/mastodon-android/commit/4ab9e25fec4fd9c10b7a8ddd1be522b3cc12cf28))
* [Make inline emoji search case-insensitive and don't only search from start of emoji names](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:better-inline-emoji-search) ([Pull request](https://github.com/mastodon/mastodon-android/pull/445))
* [Include subject line when sharing e.g. a website to Megalodon](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:external-share-include-subject)
* [Improve semantics for voting on polls (radio buttons and checkboxes)](https://github.com/sk22/megalodon/commit/6fd58c96827cb1d2da329cebdc170a1425dd18d7)
* [Copy post URL when long-pressing share button](https://github.com/sk22/megalodon/commit/ba36347f03278763ecec617b1ce57ba89db7be72)
* [Add option to disable swiping between tabs](https://github.com/sk22/megalodon/commit/1f20b21fc84bf006c1ec14bd2229cbfad5215ec8)
* [Resolve Fediverse links in the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/open-urls-in-app)
* [Preserve whitespaces in HTML](https://github.com/sk22/megalodon/commit/7d876bddc7a07d98f0fecbf62b13bdb9fcce3412)
* [Long-click to copy links](https://github.com/sk22/megalodon/commit/b32e32274923a94742a9926ef38785f746d41405)
### Visual
@@ -178,13 +123,6 @@ There's also a handful of custom strings exclusive to this projects that would n
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)
* [Custom color themes](https://github.com/sk22/megalodon/pull/124) by [@LucasGGamerM](https://github.com/LucasGGamerM)
* [Custom "megalodon" text logo](https://github.com/sk22/megalodon/commit/563afd487ca5c608cfbb00fa3909d3c27384acc0) by [@LucasGGamerM](https://github.com/LucasGGamerM)
* [Custom login screen](https://github.com/sk22/megalodon/commit/9bbf8c4618dbe13accaeb3b5482bf3fe88cac4c0)
* [More distinct filled boost icon](https://github.com/sk22/megalodon/commits/more-distinct-filled-boost-icon)
* Material You color theme by [@LucasGGamerM](https://github.com/LucasGGamerM)
* [Animations for interaction buttons](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/animate-buttons)
* [Dedicated icons for different notification types](https://github.com/sk22/megalodon/pull/178) by [@florian-obernberger](https://github.com/florian-obernberger)
## Building

View File

@@ -1,3 +0,0 @@
#!/bin/bash
find metadata -name '*.txt' -exec sed -Ei 's/^[–—─•·*]\s+/- /' {} \;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -5,14 +5,17 @@ plugins {
android {
compileSdk 33
defaultConfig {
archivesBaseName = "megalodon"
applicationId "org.joinmastodon.android.sk"
archivesBaseName = "moshidon"
applicationId "org.joinmastodon.android.moshinda"
minSdk 23
targetSdk 33
versionCode 65
versionName "1.1.5+fork.65"
versionCode 56
versionName "1.1.4+fork.56.moshinda"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
"ja-rJP", "kab", "ko-rKR", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ru-rRU",
"sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -29,11 +32,9 @@ android {
githubRelease{
initWith release
}
playRelease{
noFederatedRelease{
initWith release
minifyEnabled true
shrinkResources true
versionNameSuffix '-play'
versionNameSuffix '-nofederated'
}
}
compileOptions {
@@ -77,4 +78,4 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -111,7 +111,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
private void actuallyCheckForUpdates(){
Request req=new Request.Builder()
.url("https://api.github.com/repos/sk22/megalodon/releases/latest")
.url("https://api.github.com/repos/LucasGGamerM/moshidon/releases/latest")
.build();
Call call=MastodonAPIController.getHttpClient().newCall(req);
try(Response resp=call.execute()){
@@ -144,7 +144,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
if("megalodon.apk".equals(asset.get("name").getAsString()) && "application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
if("moshidon.apk".equals(asset.get("name").getAsString()) && "application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
long size=asset.get("size").getAsLong();
String url=asset.get("browser_download_url").getAsString();

View File

@@ -4,8 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
@@ -15,12 +14,12 @@
<application
android:name=".MastodonApp"
android:allowBackup="true"
android:label="@string/sk_app_name"
android:label="@string/app_name"
android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:theme="@style/Theme.Mastodon.AutoLightDark.Original"
android:largeHeap="true">
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -12,6 +12,7 @@ import android.widget.Toast;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
@@ -35,10 +36,13 @@ public class ExternalShareActivity extends FragmentStackActivity{
openComposeFragment(sessions.get(0).getID());
}else{
getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
UiUtils.pickAccount(this, null, R.string.choose_account, 0,
session -> openComposeFragment(session.getID()),
b -> b.setOnCancelListener(d -> finish())
);
new M3AlertDialogBuilder(this)
.setItems(sessions.stream().map(as->"@"+as.self.username+"@"+as.domain).toArray(String[]::new), (dialog, which)->{
openComposeFragment(sessions.get(which).getID());
})
.setTitle(R.string.choose_account)
.setOnCancelListener(dialog -> finish())
.show();
}
}
}
@@ -47,11 +51,7 @@ public class ExternalShareActivity extends FragmentStackActivity{
getWindow().setBackgroundDrawable(null);
Intent intent=getIntent();
StringBuilder builder=new StringBuilder();
String subject = "";
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) builder.append(subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)).append("\n\n");
if (intent.hasExtra(Intent.EXTRA_TEXT)) builder.append(intent.getStringExtra(Intent.EXTRA_TEXT)).append("\n");
String text=builder.toString();
String text=intent.getStringExtra(Intent.EXTRA_TEXT);
List<Uri> mediaUris;
if(Intent.ACTION_SEND.equals(intent.getAction())){
Uri singleUri=intent.getParcelableExtra(Intent.EXTRA_STREAM);
@@ -77,8 +77,6 @@ public class ExternalShareActivity extends FragmentStackActivity{
args.putString("account", accountID);
if(!TextUtils.isEmpty(text))
args.putString("prefilledText", text);
if(!subject.isBlank())
args.putInt("selectionEnd", subject.length());
if(mediaUris!=null && !mediaUris.isEmpty())
args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris));
Fragment fragment=new ComposeFragment();

View File

@@ -1,18 +1,8 @@
package org.joinmastodon.android;
import static org.joinmastodon.android.api.MastodonAPIController.gson;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
@@ -20,32 +10,16 @@ public class GlobalUserPreferences{
public static boolean showReplies;
public static boolean showBoosts;
public static boolean loadNewPosts;
public static boolean showFederatedTimeline;
public static boolean showInteractionCounts;
public static boolean alwaysExpandContentWarnings;
public static boolean disableMarquee;
public static boolean disableSwipe;
public static boolean voteButtonForSingleChoice;
public static boolean enableDeleteNotifications;
public static boolean translateButtonOpenedOnly;
public static boolean uniformNotificationIcon;
public static boolean reduceMotion;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
public static Map<String, List<String>> recentLanguages;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
}
private static <T> T fromJson(String json, Type type, T orElse) {
try { return gson.fromJson(json, type); }
catch (JsonSyntaxException ignored) { return orElse; }
}
public static void load(){
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
@@ -54,26 +28,11 @@ public class GlobalUserPreferences{
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
showFederatedTimeline=prefs.getBoolean("showFederatedTimeline", !BuildConfig.BUILD_TYPE.equals("playRelease"));
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false);
disableSwipe=prefs.getBoolean("disableSwipe", false);
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
reduceMotion=prefs.getBoolean("reduceMotion", false);
publishButtonText=prefs.getString("publishButtonText", "");
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>());
try {
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
} catch (IllegalArgumentException|ClassCastException ignored) {
// invalid color name or color was previously saved as integer
color=ColorPreference.PINK;
}
color=ColorPreference.values()[prefs.getInt("color", 1)];
}
public static void save(){
@@ -83,31 +42,21 @@ public class GlobalUserPreferences{
.putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts)
.putBoolean("loadNewPosts", loadNewPosts)
.putBoolean("showFederatedTimeline", showFederatedTimeline)
.putBoolean("trueBlackTheme", trueBlackTheme)
.putBoolean("showInteractionCounts", showInteractionCounts)
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
.putBoolean("disableMarquee", disableMarquee)
.putBoolean("disableSwipe", disableSwipe)
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("translateButtonOpenedOnly", translateButtonOpenedOnly)
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
.putBoolean("reduceMotion", reduceMotion)
.putString("publishButtonText", publishButtonText)
.putInt("theme", theme.ordinal())
.putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages))
.putInt("color", color.ordinal())
.apply();
}
public enum ColorPreference{
MATERIAL3,
PINK,
PURPLE,
GREEN,
BLUE,
BROWN,
RED,
ORANGE,
YELLOW
}

View File

@@ -14,9 +14,9 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
@@ -33,7 +33,7 @@ public class MainActivity extends FragmentStackActivity{
if(savedInstanceState==null){
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment());
showFragmentClearingBackStack(new SplashFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session;

View File

@@ -64,7 +64,7 @@ public class OAuthActivity extends Activity{
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account account){
AccountSessionManager.getInstance().addAccount(instance, token, account, app, null);
AccountSessionManager.getInstance().addAccount(instance, token, account, app, true);
progress.dismiss();
finish();
// not calling restartMainActivity() here on purpose to have it recreated (notice different flags)

View File

@@ -8,7 +8,9 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -142,15 +144,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true)
.setColor(context.getColor(R.color.primary_700));
if (!GlobalUserPreferences.uniformNotificationIcon) switch (pn.notificationType) {
case FAVORITE -> builder.setSmallIcon(R.drawable.ic_fluent_star_24_filled);
case REBLOG -> builder.setSmallIcon(R.drawable.ic_fluent_arrow_repeat_all_24_filled);
case FOLLOW -> builder.setSmallIcon(R.drawable.ic_fluent_person_add_24_filled);
case MENTION -> builder.setSmallIcon(R.drawable.ic_fluent_mention_24_filled);
case POLL -> builder.setSmallIcon(R.drawable.ic_fluent_poll_24_filled);
}
if(avatar!=null){
builder.setLargeIcon(UiUtils.getBitmapFromDrawable(avatar));
}

View File

@@ -20,7 +20,6 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import androidx.annotation.CallSuper;
import androidx.annotation.StringRes;
@@ -102,14 +101,9 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
return wrapProgress(activity, message, cancelable, null);
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable, Consumer<ProgressDialog> transform){
progressDialog=new ProgressDialog(activity);
progressDialog.setMessage(activity.getString(message));
progressDialog.setCancelable(cancelable);
if (transform != null) transform.accept(progressDialog);
if(cancelable){
progressDialog.setOnCancelListener(dialog->cancel());
}

View File

@@ -370,7 +370,7 @@ public class PushSubscriptionManager{
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
if(session.pushSubscription==null || forceReRegister)
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);
else
else if(session.needUpdatePushSettings)
session.getPushSubscriptionManager().updatePushSettings(session.pushSubscription);
}
}

View File

@@ -9,31 +9,23 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.util.HashMap;
import java.util.function.Consumer;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class StatusInteractionController{
private final String accountID;
private final boolean updateCounters;
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
public StatusInteractionController(String accountID, boolean updateCounters) {
this.accountID=accountID;
this.updateCounters=updateCounters;
}
public StatusInteractionController(String accountID){
this(accountID, true);
this.accountID=accountID;
}
public void setFavorited(Status status, boolean favorited, Consumer<Status> cb){
public void setFavorited(Status status, boolean favorited){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
@@ -46,9 +38,7 @@ public class StatusInteractionController{
@Override
public void onSuccess(Status result){
runningFavoriteRequests.remove(status.id);
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -56,17 +46,24 @@ public class StatusInteractionController{
runningFavoriteRequests.remove(status.id);
error.showToast(MastodonApp.context);
status.favourited=!favorited;
cb.accept(status);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
if(favorited)
status.favouritesCount--;
else
status.favouritesCount++;
E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningFavoriteRequests.put(status.id, req);
status.favourited=favorited;
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
if(favorited)
status.favouritesCount++;
else
status.favouritesCount--;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
public void setReblogged(Status status, boolean reblogged){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
@@ -74,15 +71,12 @@ public class StatusInteractionController{
if(current!=null){
current.cancel();
}
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged, visibility)
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status reblog){
Status result = reblog.getContentStatus();
public void onSuccess(Status result){
runningReblogRequests.remove(status.id);
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -90,21 +84,24 @@ public class StatusInteractionController{
runningReblogRequests.remove(status.id);
error.showToast(MastodonApp.context);
status.reblogged=!reblogged;
cb.accept(status);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
if(reblogged)
status.reblogsCount--;
else
status.reblogsCount++;
E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningReblogRequests.put(status.id, req);
status.reblogged=reblogged;
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
if(reblogged)
status.reblogsCount++;
else
status.reblogsCount--;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setBookmarked(Status status, boolean bookmarked){
setBookmarked(status, bookmarked, r->{});
}
public void setBookmarked(Status status, boolean bookmarked, Consumer<Status> cb){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
@@ -117,8 +114,7 @@ public class StatusInteractionController{
@Override
public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id);
cb.accept(result);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -126,13 +122,12 @@ public class StatusInteractionController{
runningBookmarkRequests.remove(status.id);
error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked;
cb.accept(status);
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked;
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
E.post(new StatusCountersUpdatedEvent(status));
}
}

View File

@@ -7,15 +7,4 @@ public class GetInstance extends MastodonAPIRequest<Instance>{
public GetInstance(){
super(HttpMethod.GET, "/instance", Instance.class);
}
public static class V2 extends MastodonAPIRequest<Instance.V2>{
public V2(){
super(HttpMethod.GET, "/instance", Instance.V2.class);
}
@Override
protected String getPathPrefix() {
return "/api/v2";
}
}
}

View File

@@ -1,17 +0,0 @@
package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import java.util.EnumSet;
import java.util.List;
public class DismissNotification extends MastodonAPIRequest<Object>{
public DismissNotification(String id){
super(HttpMethod.POST, "/notifications/" + (id != null ? id + "/dismiss" : "clear"), Object.class);
setRequestBody(new Object());
}
}

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
@@ -10,29 +9,12 @@ import java.util.ArrayList;
import java.util.List;
public class CreateStatus extends MastodonAPIRequest<Status>{
public static final Instant DRAFTS_AFTER_INSTANT = Instant.ofEpochMilli(253370764799999L) /* end of 9998 */;
private static final float draftFactor = 31536000000f /* one year */ / 253370764799999f /* end of 9998 */;
public static Instant getDraftInstant() {
// returns an instant between 9999-01-01 00:00:00 and 9999-12-31 23:59:59
// yes, this is a weird implementation for something that hardly matters
return DRAFTS_AFTER_INSTANT.plusMillis(1 + (long) (System.currentTimeMillis() * draftFactor));
}
public CreateStatus(CreateStatus.Request req, String uuid){
super(HttpMethod.POST, "/statuses", Status.class);
setRequestBody(req);
addHeader("Idempotency-Key", uuid);
}
public static class Scheduled extends MastodonAPIRequest<ScheduledStatus>{
public Scheduled(CreateStatus.Request req, String uuid){
super(HttpMethod.POST, "/statuses", ScheduledStatus.class);
setRequestBody(req);
addHeader("Idempotency-Key", uuid);
}
}
public static class Request{
public String status;
public List<String> mediaIds;

View File

@@ -7,10 +7,4 @@ public class DeleteStatus extends MastodonAPIRequest<Status>{
public DeleteStatus(String id){
super(HttpMethod.DELETE, "/statuses/"+id, Status.class);
}
public static class Scheduled extends MastodonAPIRequest<Object> {
public Scheduled(String id) {
super(HttpMethod.DELETE, "/scheduled_statuses/"+id, Object.class);
}
}
}

View File

@@ -1,16 +0,0 @@
package org.joinmastodon.android.api.requests.statuses;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.ScheduledStatus;
public class GetScheduledStatuses extends HeaderPaginationRequest<ScheduledStatus>{
public GetScheduledStatuses(String maxID, int limit){
super(HttpMethod.GET, "/scheduled_statuses", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
}
}

View File

@@ -2,17 +2,10 @@ package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
public class SetStatusReblogged extends MastodonAPIRequest<Status>{
public SetStatusReblogged(String id, boolean reblogged, StatusPrivacy visibility){
public SetStatusReblogged(String id, boolean reblogged){
super(HttpMethod.POST, "/statuses/"+id+"/"+(reblogged ? "reblog" : "unreblog"), Status.class);
Request req = new Request();
req.visibility = visibility;
setRequestBody(req);
}
public static class Request {
public StatusPrivacy visibility;
setRequestBody(new Object());
}
}

View File

@@ -1,11 +0,0 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.TranslatedStatus;
public class TranslateStatus extends MastodonAPIRequest<TranslatedStatus> {
public TranslateStatus(String id) {
super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class);
setRequestBody(new Object());
}
}

View File

@@ -1,24 +0,0 @@
package org.joinmastodon.android.api.requests.tags;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.model.Hashtag;
import java.util.List;
public class GetFollowedHashtags extends HeaderPaginationRequest<Hashtag> {
public GetFollowedHashtags(String maxID, String minID, int limit, String sinceID){
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(sinceID!=null)
addQueryParameter("since_id", sinceID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
}

View File

@@ -1,11 +0,0 @@
package org.joinmastodon.android.api.session;
public class AccountActivationInfo{
public String email;
public long lastEmailConfirmationResend;
public AccountActivationInfo(String email, long lastEmailConfirmationResend){
this.email=email;
this.lastEmailConfirmationResend=lastEmailConfirmationResend;
}
}

View File

@@ -7,7 +7,6 @@ import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Token;
@@ -29,20 +28,17 @@ public class AccountSession{
public long filtersLastUpdated;
public List<Filter> wordFilters=new ArrayList<>();
public String pushAccountID;
public Preferences preferences;
public AccountActivationInfo activationInfo;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
AccountSession(Token token, Account self, Application app, String domain, boolean activated){
this.token=token;
this.self=self;
this.domain=domain;
this.app=app;
this.activated=activated;
this.activationInfo=activationInfo;
infoLastUpdated=System.currentTimeMillis();
}
@@ -52,10 +48,6 @@ public class AccountSession{
return domain+"_"+self.id;
}
public String getFullUsername() {
return "@"+self.username+"@"+domain;
}
public MastodonAPIController getApiController(){
if(apiController==null)
apiController=new MastodonAPIController(this);
@@ -68,12 +60,6 @@ public class AccountSession{
return statusInteractionController;
}
public StatusInteractionController getRemoteStatusInteractionController(){
if(remoteStatusInteractionController==null)
remoteStatusInteractionController=new StatusInteractionController(getID(), false);
return remoteStatusInteractionController;
}
public CacheController getCacheController(){
if(cacheController==null)
cacheController=new CacheController(getID());

View File

@@ -13,6 +13,8 @@ import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
@@ -20,7 +22,6 @@ import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
@@ -33,7 +34,6 @@ import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token;
import java.io.File;
@@ -100,13 +100,13 @@ public class AccountSessionManager{
maybeUpdateShortcuts();
}
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
instances.put(instance.uri, instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
AccountSession session=new AccountSession(token, self, app, instance.uri, active);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
updateMoreInstanceInfo(instance, instance.uri);
updateInstanceEmojis(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
@@ -248,13 +248,12 @@ public class AccountSessionManager{
HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase());
// if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionPreferences(session);
updateSessionLocalInfo(session);
// }
// if(now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session);
// }
if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionLocalInfo(session);
}
if(now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session);
}
}
if(loadedInstances){
maybeUpdateCustomEmojis(domains);
@@ -264,10 +263,10 @@ public class AccountSessionManager{
private void maybeUpdateCustomEmojis(Set<String> domains){
long now=System.currentTimeMillis();
for(String domain:domains){
// Long lastUpdated=instancesLastUpdated.get(domain);
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateInstanceInfo(domain);
// }
Long lastUpdated=instancesLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateInstanceInfo(domain);
}
}
}
@@ -289,18 +288,6 @@ public class AccountSessionManager{
.exec(session.getID());
}
private void updateSessionPreferences(AccountSession session){
new GetPreferences().setCallback(new Callback<>() {
@Override
public void onSuccess(Preferences preferences) {
session.preferences=preferences;
}
@Override
public void onError(ErrorResponse error) {}
}).exec(session.getID());
}
private void updateSessionWordFilters(AccountSession session){
new GetWordFilters()
.setCallback(new Callback<>(){
@@ -325,7 +312,7 @@ public class AccountSessionManager{
@Override
public void onSuccess(Instance instance){
instances.put(domain, instance);
updateMoreInstanceInfo(instance, domain);
updateInstanceEmojis(instance, domain);
}
@Override
@@ -336,21 +323,6 @@ public class AccountSessionManager{
.execNoAuth(domain);
}
public void updateMoreInstanceInfo(Instance instance, String domain) {
new GetInstance.V2().setCallback(new Callback<>() {
@Override
public void onSuccess(Instance.V2 v2) {
if (instance != null) instance.v2 = v2;
updateInstanceEmojis(instance, domain);
}
@Override
public void onError(ErrorResponse errorResponse) {
updateInstanceEmojis(instance, domain);
}
}).execNoAuth(instance.uri);
}
private void updateInstanceEmojis(Instance instance, String domain){
new GetCustomEmojis()
.setCallback(new Callback<>(){
@@ -426,10 +398,6 @@ public class AccountSessionManager{
return instances.get(domain);
}
public Instance getInstanceInfoForAccount(String account) {
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
}
public void updateAccountInfo(String id, Account account){
AccountSession session=getAccount(id);
session.self=account;

View File

@@ -1,13 +0,0 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ScheduledStatus;
public class ScheduledStatusCreatedEvent {
public final ScheduledStatus scheduledStatus;
public final String accountID;
public ScheduledStatusCreatedEvent(ScheduledStatus scheduledStatus, String accountID){
this.scheduledStatus = scheduledStatus;
this.accountID=accountID;
}
}

View File

@@ -1,13 +0,0 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ScheduledStatus;
public class ScheduledStatusDeletedEvent{
public final String id;
public final String accountID;
public ScheduledStatusDeletedEvent(String id, String accountID){
this.id=id;
this.accountID=accountID;
}
}

View File

@@ -400,12 +400,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
Poll poll=holder.getItem().poll;
Poll.Option option=holder.getItem().option;
if(poll.multiple || GlobalUserPreferences.voteButtonForSingleChoice){
if(poll.multiple){
if(poll.selectedOptions==null)
poll.selectedOptions=new ArrayList<>();
boolean optionContained=poll.selectedOptions.contains(option);
if(!poll.multiple) poll.selectedOptions.clear();
if(optionContained){
if(poll.selectedOptions.contains(option)){
poll.selectedOptions.remove(option);
holder.itemView.setSelected(false);
}else{
@@ -414,9 +412,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
if(!poll.multiple && vh instanceof PollOptionStatusDisplayItem.Holder item){
if (item != holder) item.itemView.setSelected(false);
}
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
if(footer.getItemID().equals(holder.getItemID())){
footer.rebind();
@@ -787,7 +782,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentMediaHiddenLayoutsWidth=width;
String title=getString(R.string.sensitive_content);
TextPaint titlePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
titlePaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray50));
titlePaint.setColor(getResources().getColor(R.color.gray_50));
titlePaint.setTextSize(V.dp(22));
titlePaint.setTypeface(mediumTypeface);
mediaHiddenTitleLayout=StaticLayout.Builder.obtain(title, 0, title.length(), titlePaint, width)
@@ -798,7 +793,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray200));
textPaint.setColor(getResources().getColor(R.color.gray_200));
textPaint.setTextSize(V.dp(16));
String text=getString(R.string.sensitive_content_explain);
mediaHiddenTextLayout=StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, width)

View File

@@ -1,16 +1,8 @@
package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
@@ -33,18 +25,15 @@ import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
@@ -52,7 +41,6 @@ import android.view.WindowManager;
import android.view.animation.LinearInterpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
@@ -60,28 +48,24 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.twitter.twittertext.TwitterTextEmojiRegex;
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.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.ProgressListener;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.EditStatus;
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
@@ -93,7 +77,6 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.ComposeAutocompleteViewController;
@@ -109,27 +92,18 @@ import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ComposeEditText;
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import org.joinmastodon.android.utils.MastodonLanguage;
import org.parceler.Parcel;
import org.parceler.Parcels;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -147,7 +121,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private static final int MEDIA_RESULT=717;
private static final int IMAGE_DESCRIPTION_RESULT=363;
private static final int SCHEDULED_STATUS_OPENED_RESULT=161;
private static final int MAX_ATTACHMENTS=4;
private static final String TAG="ComposeFragment";
@@ -171,9 +144,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private String accountID;
private int charCount, charLimit, trimmedCharCount;
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss;
private Button publishButton;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
private ImageView sensitiveIcon;
private ComposeMediaLayout attachmentsView;
private TextView replyText;
@@ -181,14 +153,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private View pollWrap;
private View addPollOptionBtn;
private View sensitiveItem;
private View pollAllowMultipleItem;
private View scheduleDraftView;
private ScrollView scrollView;
private boolean initiallyScrolled = false;
private TextView scheduleDraftText;
private CheckBox pollAllowMultipleCheckbox;
private TextView pollDurationView;
private MenuItem draftMenuItem, undraftMenuItem, scheduleMenuItem, unscheduleMenuItem;
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
@@ -204,7 +169,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private EditText spoilerEdit;
private boolean hasSpoiler;
private boolean sensitive;
private Instant scheduledAt = null;
private ProgressBar sendProgress;
private ImageView sendError;
private View sendingOverlay;
@@ -217,16 +181,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private boolean attachmentsErrorShowing;
private Status editingStatus;
private ScheduledStatus scheduledStatus;
private boolean redraftStatus;
private boolean pollChanged;
private boolean creatingView;
private boolean ignoreSelectionChanges=false;
private Runnable updateUploadEtaRunnable;
private String language;
private MastodonLanguage.LanguageResolver languageResolver;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -238,10 +198,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
instanceDomain=session.domain;
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
languageResolver=new MastodonLanguage.LanguageResolver(instance);
redraftStatus=getArguments().getBoolean("redraftStatus", false);
if(getArguments().containsKey("editStatus")){
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
redraftStatus=getArguments().getBoolean("redraftStatus");
}
if(instance==null){
Nav.finish(this);
@@ -251,16 +210,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
}
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
if (bundle.containsKey("scheduledAt")) scheduledAt=(Instant) bundle.getSerializable("scheduledAt");
if(instance.maxTootChars>0)
charLimit=instance.maxTootChars;
else if(instance.configuration!=null && instance.configuration.statuses!=null && instance.configuration.statuses.maxCharacters>0)
charLimit=instance.configuration.statuses.maxCharacters;
else
charLimit=500;
loadDefaultStatusVisibility(savedInstanceState);
}
@Override
@@ -283,7 +240,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
wm=activity.getSystemService(WindowManager.class);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
creatingView=true;
@@ -295,11 +251,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mainEditTextWrap=view.findViewById(R.id.toot_text_wrap);
charCounter=view.findViewById(R.id.char_counter);
charCounter.setText(String.valueOf(charLimit));
scrollView=view.findViewById(R.id.scroll_view);
selfName=view.findViewById(R.id.self_name);
selfUsername=view.findViewById(R.id.self_username);
selfAvatar=view.findViewById(R.id.self_avatar);
selfName=view.findViewById(R.id.name);
selfUsername=view.findViewById(R.id.username);
selfAvatar=view.findViewById(R.id.avatar);
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
selfUsername.setText('@'+self.username+'@'+instanceDomain);
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
@@ -317,10 +272,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn);
sensitiveIcon=view.findViewById(R.id.sensitive_icon);
sensitiveItem=view.findViewById(R.id.sensitive_item);
replyText=view.findViewById(R.id.reply_text);
@@ -329,13 +280,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
buildVisibilityPopup(visibilityBtn);
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
visibilityBtn.setOnClickListener(this::onVisibilityClick);
sensitiveItem.setOnClickListener(v->toggleSensitive());
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
@@ -352,9 +297,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollOptionsView=view.findViewById(R.id.poll_options);
pollWrap=view.findViewById(R.id.poll_wrap);
addPollOptionBtn=view.findViewById(R.id.add_poll_option);
pollAllowMultipleItem=view.findViewById(R.id.poll_allow_multiple);
pollAllowMultipleCheckbox=view.findViewById(R.id.poll_allow_multiple_checkbox);
pollAllowMultipleItem.setOnClickListener(v->this.togglePollAllowMultiple());
addPollOptionBtn.setOnClickListener(v->{
createDraftPollOption().edit.requestFocus();
@@ -369,7 +311,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollBtn.setSelected(true);
mediaBtn.setEnabled(false);
pollWrap.setVisibility(View.VISIBLE);
updatePollAllowMultiple(savedInstanceState.getBoolean("pollAllowMultiple", false));
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){
DraftPollOption opt=createDraftPollOption();
opt.edit.setText(oldText);
@@ -380,17 +321,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollBtn.setSelected(true);
mediaBtn.setEnabled(false);
pollWrap.setVisibility(View.VISIBLE);
updatePollAllowMultiple(editingStatus.poll.multiple);
for(Poll.Option eopt:editingStatus.poll.options){
DraftPollOption opt=createDraftPollOption();
opt.edit.setText(eopt.title);
}
pollDuration=scheduledStatus == null
? (int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond()
: Integer.parseInt(scheduledStatus.params.poll.expiresIn);
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), scheduledStatus == null
? editingStatus.poll.expiresAt
: Instant.now().plus(pollDuration, ChronoUnit.SECONDS));
pollDuration=(int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond();
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), editingStatus.poll.expiresAt);
updatePollOptionHints();
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
}else{
@@ -429,17 +365,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
} else {
loadDefaultStatusVisibility(savedInstanceState);
}
updateVisibilityIcon();
visibilityPopup.getMenu().findItem(switch(statusVisibility){
case PUBLIC -> R.id.vis_public;
case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private;
}).setChecked(true);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
@@ -463,11 +391,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putStringArrayList("pollOptions", opts);
outState.putInt("pollDuration", pollDuration);
outState.putString("pollDurationStr", pollDurationStr);
outState.putBoolean("pollAllowMultiple", pollAllowMultipleItem.isSelected());
}
outState.putBoolean("sensitive", sensitive);
outState.putBoolean("hasSpoiler", hasSpoiler);
outState.putString("language", language);
if(!attachments.isEmpty()){
ArrayList<Parcelable> serializedAttachments=new ArrayList<>(attachments.size());
for(DraftMediaAttachment att:attachments){
@@ -476,8 +402,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putParcelableArrayList("attachments", serializedAttachments);
}
outState.putSerializable("visibility", statusVisibility);
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
}
@Override
@@ -560,86 +484,32 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
});
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
if(replyTo!=null){
View replyWrap = view.findViewById(R.id.reply_wrap);
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
int scrollHeight = scrollView.getHeight();
if (replyWrap.getMinimumHeight() != scrollHeight) {
replyWrap.setMinimumHeight(scrollHeight);
if (!initiallyScrolled) {
initiallyScrolled = true;
scrollView.post(() -> {
int bottom = scrollView.getChildAt(0).getBottom();
int delta = bottom - (scrollView.getScrollY() + scrollView.getHeight());
int space = GlobalUserPreferences.reduceMotion ? 0 : Math.min(V.dp(120), delta);
scrollView.scrollBy(0, delta - space);
if (!GlobalUserPreferences.reduceMotion) {
scrollView.postDelayed(() -> scrollView.smoothScrollBy(0, space), 100);
}
});
}
}
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
int visibilityNameRes = switch (statusVisibility) {
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
};
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
Drawable visibilityIcon = getActivity().getDrawable(switch(statusVisibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_at_symbol;
});
View originalPost = view.findViewById(R.id.original_post);
originalPost.setVisibility(View.VISIBLE);
originalPost.setOnClickListener(v->{
visibilityIcon.setBounds(0, 0, V.dp(20), V.dp(20));
Drawable replyArrow = getActivity().getDrawable(R.drawable.ic_fluent_arrow_reply_20_filled);
replyArrow.setBounds(0, 0, V.dp(20), V.dp(20));
replyText.setCompoundDrawables(replyArrow, null, visibilityIcon, null);
replyText.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(replyTo));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ThreadFragment.class, args);
});
ImageView avatar = view.findViewById(R.id.avatar);
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(replyTo.account.avatar));
ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12));
}
};
avatar.setOutlineProvider(roundCornersOutline);
avatar.setClipToOutline(true);
avatar.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(replyTo.account));
Nav.go(getActivity(), ProfileFragment.class, args);
});
((TextView) view.findViewById(R.id.name)).setText(replyTo.account.displayName);
((TextView) view.findViewById(R.id.username)).setText(replyTo.account.getDisplayUsername());
view.findViewById(R.id.visibility).setVisibility(View.GONE);
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
});
ImageView moreBtn = view.findViewById(R.id.more);
moreBtn.setImageDrawable(visibilityIcon);
moreBtn.setBackground(null);
TextView timestamp = view.findViewById(R.id.timestamp);
if (replyTo.editedAt==null) timestamp.setText(UiUtils.formatRelativeTimestamp(getContext(), replyTo.createdAt));
else timestamp.setText(getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(getContext(), replyTo.editedAt)));
if (replyTo.spoilerText != null && !replyTo.spoilerText.isBlank()) {
view.findViewById(R.id.spoiler_header).setVisibility(View.VISIBLE);
((TextView) view.findViewById(R.id.spoiler_title_inline)).setText(replyTo.spoilerText);
}
SpannableStringBuilder content = HtmlParser.parse(replyTo.content, replyTo.emojis, replyTo.mentions, replyTo.tags, accountID);
LinkedTextView text = view.findViewById(R.id.text);
if (content.length() > 0) text.setText(content);
else view.findViewById(R.id.display_item_text).setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
int visibilityNameRes = switch (replyTo.visibility) {
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
};
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
ArrayList<String> mentions=new ArrayList<>();
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!replyTo.account.id.equals(ownID))
@@ -663,7 +533,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerEdit.setText(replyTo.spoilerText);
spoilerBtn.setSelected(true);
}
if (replyTo.language != null && !replyTo.language.isEmpty()) updateLanguage(replyTo.language);
}
}else if (editingStatus==null || editingStatus.inReplyToId==null){
// TODO: remove workaround after https://github.com/mastodon/mastodon-android/issues/341 gets fixed
@@ -676,7 +545,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
ignoreSelectionChanges=true;
mainEditText.setSelection(mainEditText.length());
ignoreSelectionChanges=false;
updateLanguage(editingStatus.language);
if(!editingStatus.mediaAttachments.isEmpty()){
attachmentsView.setVisibility(View.VISIBLE);
for(Attachment att:editingStatus.mediaAttachments){
@@ -699,11 +567,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
ignoreSelectionChanges=false;
initialText=prefilledText;
}
if (getArguments().containsKey("selectionStart") || getArguments().containsKey("selectionEnd")) {
int selectionStart=getArguments().getInt("selectionStart", 0);
int selectionEnd=getArguments().getInt("selectionEnd", selectionStart);
mainEditText.setSelection(selectionStart, selectionEnd);
}
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
if(mediaUris!=null && !mediaUris.isEmpty()){
for(Uri uri:mediaUris){
@@ -723,95 +586,34 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save);
publishButton=new Button(getActivity());
publishButton.setText(editingStatus==null || redraftStatus ? R.string.publish : R.string.save);
publishButton.setOnClickListener(this::onPublishClick);
LinearLayout wrap=new LinearLayout(getActivity());
getActivity().getLayoutInflater().inflate(R.layout.compose_action, wrap);
wrap.setOrientation(LinearLayout.HORIZONTAL);
sendProgress=new ProgressBar(getActivity());
LinearLayout.LayoutParams progressLP=new LinearLayout.LayoutParams(V.dp(24), V.dp(24));
progressLP.setMarginEnd(V.dp(16));
progressLP.gravity=Gravity.CENTER_VERTICAL;
wrap.addView(sendProgress, progressLP);
sendError=new ImageView(getActivity());
sendError.setImageResource(R.drawable.ic_fluent_error_circle_24_regular);
sendError.setImageTintList(getResources().getColorStateList(R.color.error_600));
sendError.setScaleType(ImageView.ScaleType.CENTER);
wrap.addView(sendError, progressLP);
sendError.setVisibility(View.GONE);
sendProgress.setVisibility(View.GONE);
wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
wrap.setClipToPadding(false);
MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
draftsBtn = wrap.findViewById(R.id.drafts_btn);
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
draftOptionsPopup.inflate(R.menu.compose_more);
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
draftOptionsPopup.setOnMenuItemClickListener(i->{
int id = i.getItemId();
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
else if (id == R.id.schedule) pickScheduledDateTime();
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
else navigateToUnsentPosts();
return true;
});
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
publishButton = wrap.findViewById(R.id.publish_btn);
languageButton = wrap.findViewById(R.id.language_btn);
sendProgress = wrap.findViewById(R.id.send_progress);
sendError = wrap.findViewById(R.id.send_error);
publishButton.setOnClickListener(this::onPublishClick);
draftsBtn.setOnClickListener(v-> draftOptionsPopup.show());
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
}
private void navigateToUnsentPosts() {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("hide_fab", true);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(draftsBtn.getWindowToken(), 0);
if (hasDraft()) {
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
} else {
// result for the previous ScheduledStatusList
setResult(true, null);
// finishing fragment in "onFragmentResult"
Nav.goForResult(getActivity(), ScheduledStatusListFragment.class, args, SCHEDULED_STATUS_OPENED_RESULT, this);
}
}
private void updateLanguage(String lang) {
updateLanguage(lang == null ? languageResolver.getDefault() : languageResolver.from(lang));
}
private void updateLanguage(MastodonLanguage loc) {
language = loc.getLanguage();
languageButton.setText(loc.getLanguageName());
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, loc.getDefaultName()));
}
@SuppressLint("ClickableViewAccessibility")
private void buildLanguageSelector(Button btn) {
languagePopup=new PopupMenu(getActivity(), languageButton);
btn.setOnTouchListener(languagePopup.getDragToOpenListener());
btn.setOnClickListener(v->languagePopup.show());
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
? languageResolver.from(prefs.postingDefaultLanguage)
: languageResolver.getDefault());
Menu languageMenu = languagePopup.getMenu();
for (String recentLanguage : Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)) {
MastodonLanguage l = languageResolver.from(recentLanguage);
languageMenu.add(0, allLanguages.indexOf(l), Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
}
SubMenu allLanguagesMenu = languageMenu.addSubMenu(R.string.sk_available_languages);
for (int i = 0; i < allLanguages.size(); i++) {
MastodonLanguage l = allLanguages.get(i);
allLanguagesMenu.add(0, i, Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
}
languagePopup.setOnMenuItemClickListener(i->{
if (i.hasSubMenu()) return false;
updateLanguage(allLanguages.get(i.getItemId()));
return true;
});
updatePublishButtonState();
}
@Override
@@ -848,15 +650,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updatePublishButtonState();
}
private void resetPublishButtonText() {
int publishText = editingStatus==null || redraftStatus ? R.string.publish : R.string.save;
if (publishText == R.string.publish && !GlobalUserPreferences.publishButtonText.isEmpty()) {
publishButton.setText(GlobalUserPreferences.publishButtonText);
} else {
publishButton.setText(publishText);
}
}
private void updatePublishButtonState(){
uuid=null;
int nonEmptyPollOptionsCount=0;
@@ -872,7 +665,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
nonDoneAttachmentCount++;
}
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
sendError.setVisibility(View.GONE);
}
private void onCustomEmojiClick(Emoji emoji){
@@ -891,51 +683,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
publish();
}
private void publishErrorCallback(ErrorResponse error) {
wm.removeView(sendingOverlay);
sendingOverlay=null;
sendProgress.setVisibility(View.GONE);
sendError.setVisibility(View.VISIBLE);
publishButton.setEnabled(true);
if (error != null) error.showToast(getActivity());
}
private void createScheduledStatusFinish(ScheduledStatus result) {
wm.removeView(sendingOverlay);
sendingOverlay=null;
Toast.makeText(getContext(), scheduledAt.isAfter(DRAFTS_AFTER_INSTANT) ?
R.string.sk_draft_saved : R.string.sk_post_scheduled, Toast.LENGTH_SHORT).show();
Nav.finish(ComposeFragment.this);
E.post(new ScheduledStatusCreatedEvent(result, accountID));
}
private void maybeDeleteScheduledPost(Runnable callback) {
if (scheduledStatus != null) {
new DeleteStatus.Scheduled(scheduledStatus.id).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
E.post(new ScheduledStatusDeletedEvent(scheduledStatus.id, accountID));
callback.run();
}
@Override
public void onError(ErrorResponse error) {
publishErrorCallback(error);
}
}).exec(accountID);
} else {
callback.run();
}
}
private void publish(){
String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request();
req.status=text;
req.visibility=statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.scheduledAt = scheduledAt;
if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
}
@@ -945,7 +698,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(!pollOptions.isEmpty()){
req.poll=new CreateStatus.Request.Poll();
req.poll.expiresIn=pollDuration;
req.poll.multiple=pollAllowMultipleItem.isSelected();
for(DraftPollOption opt:pollOptions)
req.poll.options.add(opt.edit.getText().toString());
}
@@ -972,32 +724,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Callback<Status> resCallback=new Callback<>(){
@Override
public void onSuccess(Status result){
maybeDeleteScheduledPost(() -> {
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));
}
}else{
E.post(new StatusUpdatedEvent(result));
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));
}
Nav.finish(ComposeFragment.this);
if (getArguments().getBoolean("navigateToStatus", false)) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
Nav.go(getActivity(), ThreadFragment.class, args);
}
});
}else{
E.post(new StatusUpdatedEvent(result));
}
Nav.finish(ComposeFragment.this);
if (getArguments().getBoolean("navigateToStatus", false)) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
Nav.go(getActivity(), ThreadFragment.class, args);
}
}
@Override
public void onError(ErrorResponse error){
publishErrorCallback(error);
wm.removeView(sendingOverlay);
sendingOverlay=null;
sendProgress.setVisibility(View.GONE);
sendError.setVisibility(View.VISIBLE);
publishButton.setEnabled(true);
error.showToast(getActivity());
}
};
@@ -1005,45 +760,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
new EditStatus(req, editingStatus.id)
.setCallback(resCallback)
.exec(accountID);
}else if(req.scheduledAt == null){
}else{
new CreateStatus(req, uuid)
.setCallback(resCallback)
.exec(accountID);
}else if(req.scheduledAt.isAfter(Instant.now().plus(10, ChronoUnit.MINUTES))){
// checking for 10 instead of 5 minutes (as per mastodon) because i really don't want
// bugs to occur because the client's clock is wrong by a minute or two - the api
// returns a status instead of a scheduled status if scheduled time is less than 5
// minutes into the future and this is 1. unexpected for the user and 2. hard to handle
new CreateStatus.Scheduled(req, uuid)
.setCallback(new Callback<>() {
@Override
public void onSuccess(ScheduledStatus result) {
maybeDeleteScheduledPost(() -> {
createScheduledStatusFinish(result);
});
}
@Override
public void onError(ErrorResponse error) {
publishErrorCallback(error);
}
}).exec(accountID);
}else{
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_scheduled_too_soon_title)
.setMessage(R.string.sk_scheduled_too_soon)
.setPositiveButton(R.string.ok, (a, b)->{})
.show();
publishErrorCallback(null);
publishButton.setEnabled(false);
}
if (replyTo == null) {
List<String> newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages));
newRecentLanguages.remove(language);
newRecentLanguages.add(0, language);
recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList()));
GlobalUserPreferences.save();
}
}
@@ -1055,8 +775,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
List<String> existingMediaIDs=editingStatus.mediaAttachments.stream().map(a->a.id).collect(Collectors.toList());
if(!existingMediaIDs.equals(attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList())))
return true;
if(!statusVisibility.equals(editingStatus.visibility)) return true;
if(scheduledStatus != null && !scheduledStatus.scheduledAt.equals(scheduledAt)) return true;
return pollChanged;
}
boolean pollFieldsHaveContent=false;
@@ -1101,19 +819,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
break;
}
}
} else if (reqCode == SCHEDULED_STATUS_OPENED_RESULT && success && getActivity() != null) {
Nav.finish(this);
}
}
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
.setPositiveButton(R.string.save, (d, w) -> {
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
publish();
})
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
.setNegativeButton(R.string.cancel, null)
.show();
}
@@ -1292,10 +1005,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void onProgress(long transferred, long total){
if(updateUploadEtaRunnable==null){
// getting a NoSuchMethodError: No static method -$$Nest$mupdateUploadETAs(ComposeFragment;)V in class ComposeFragment
// when using method reference out of nowhere after changing code elsewhere. no idea. programming is awful, actually
// noinspection Convert2MethodRef
UiUtils.runOnUiThread(updateUploadEtaRunnable=()->ComposeFragment.this.updateUploadETAs(), 50);
UiUtils.runOnUiThread(updateUploadEtaRunnable=ComposeFragment.this::updateUploadETAs, 100);
}
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
if(Build.VERSION.SDK_INT>=24)
@@ -1458,7 +1168,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
att.uploadStateText.setText(getString(R.string.file_upload_time_remaining, time));
}
}
UiUtils.runOnUiThread(updateUploadEtaRunnable, 50);
UiUtils.runOnUiThread(updateUploadEtaRunnable, 100);
}
private void onEditMediaDescriptionClick(View v){
@@ -1498,11 +1208,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
option.view=LayoutInflater.from(getActivity()).inflate(R.layout.compose_poll_option, pollOptionsView, false);
option.edit=option.view.findViewById(R.id.edit);
option.dragger=option.view.findViewById(R.id.dragger_thingy);
ImageView icon = option.view.findViewById(R.id.icon);
icon.setImageDrawable(getContext().getDrawable(pollAllowMultipleItem.isSelected() ?
R.drawable.ic_poll_checkbox_regular_selector :
R.drawable.ic_poll_option_button
));
option.dragger.setOnLongClickListener(v->{
pollOptionsView.startDragging(option.view);
@@ -1590,69 +1295,23 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if (attachments.isEmpty()) sensitive = false;
}
private void pickScheduledDateTime() {
LocalDateTime soon = LocalDateTime.now()
.plus(15, ChronoUnit.MINUTES) // so 14:59 doesn't get rounded up to…
.plus(1, ChronoUnit.HOURS) // …15:00, but rather 16:00
.withMinute(0);
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
updateScheduledAt(LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute)
.toInstant(OffsetDateTime.now().getOffset()));
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
}
private void updateScheduledAt(Instant scheduledAt) {
this.scheduledAt = scheduledAt;
updatePublishButtonState();
scheduleDraftView.setVisibility(scheduledAt == null ? View.GONE : View.VISIBLE);
draftMenuItem.setVisible(true);
scheduleMenuItem.setVisible(true);
undraftMenuItem.setVisible(false);
unscheduleMenuItem.setVisible(false);
if (scheduledAt != null) {
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
if (scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
draftMenuItem.setVisible(false);
undraftMenuItem.setVisible(true);
scheduleTimeBtn.setVisibility(View.GONE);
scheduleDraftText.setText(R.string.sk_compose_draft);
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_drafts_20_regular, 0, 0, 0);
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_draft));
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_drafts_20_filled, 0, 0, 0);
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)
? R.string.save : R.string.sk_draft);
} else {
scheduleMenuItem.setVisible(false);
unscheduleMenuItem.setVisible(true);
String at = scheduledAt.atZone(ZoneId.systemDefault()).format(formatter);
scheduleTimeBtn.setVisibility(View.VISIBLE);
scheduleTimeBtn.setText(at);
scheduleDraftText.setText(R.string.sk_compose_scheduled);
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_schedule));
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_clock_20_filled, 0, 0, 0);
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.equals(scheduledAt)
? R.string.save : R.string.sk_schedule);
}
} else {
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_clock_20_regular, 0, 0, 0);
resetPublishButtonText();
}
}
private int getMediaAttachmentsCount(){
return attachments.size();
}
private void buildVisibilityPopup(View v){
visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu();
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
private void onVisibilityClick(View v){
PopupMenu menu=new PopupMenu(getActivity(), v);
menu.inflate(R.menu.compose_visibility);
Menu m=menu.getMenu();
UiUtils.enablePopupMenuIcons(getActivity(), menu);
m.setGroupCheckable(0, true, true);
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
m.findItem(switch(statusVisibility){
case PUBLIC -> R.id.vis_public;
case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private;
}).setChecked(true);
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
@Override
public boolean onMenuItemClick(MenuItem item){
int id=item.getItemId();
@@ -1670,6 +1329,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return true;
}
});
menu.show();
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
@@ -1683,23 +1343,34 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
if (prefs != null) {
// Only override the reply visibility if our preference is more private
if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
statusVisibility = switch (prefs.postingDefaultVisibility) {
case PUBLIC -> StatusPrivacy.PUBLIC;
case UNLISTED -> StatusPrivacy.UNLISTED;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
}
new GetPreferences()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Preferences result){
// Only override the reply visibility if our preference is more private
if (result.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
statusVisibility = switch (result.postingDefaultVisibility) {
case PUBLIC -> StatusPrivacy.PUBLIC;
case UNLISTED -> StatusPrivacy.UNLISTED;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
}
// A saved privacy setting from a previous compose session wins over all
if(savedInstanceState !=null){
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
}
// A saved privacy setting from a previous compose session wins over all
if(savedInstanceState !=null){
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
updateVisibilityIcon ();
}
@Override
public void onError(ErrorResponse error){
Log.w(TAG, "Unable to get user preferences to set default post privacy");
}
})
.exec(accountID);
}
private void updateVisibilityIcon(){
@@ -1710,31 +1381,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
case DIRECT -> R.drawable.ic_at_symbol;
});
}
private void togglePollAllowMultiple() {
updatePollAllowMultiple(!pollAllowMultipleItem.isSelected());
}
private void updatePollAllowMultiple(boolean multiple){
pollAllowMultipleItem.setSelected(multiple);
pollAllowMultipleCheckbox.setChecked(multiple);
ImageView btn = addPollOptionBtn.findViewById(R.id.add_poll_option_icon);
btn.setImageDrawable(getContext().getDrawable(multiple ?
R.drawable.ic_fluent_add_square_24_regular :
R.drawable.ic_fluent_add_circle_24_regular
));
for (DraftPollOption opt:pollOptions) {
ImageView icon = opt.view.findViewById(R.id.icon);
icon.setImageDrawable(getContext().getDrawable(multiple ?
R.drawable.ic_poll_checkbox_regular_selector :
R.drawable.ic_poll_option_button
));
}
}
@Override
public void onSelectionChanged(int start, int end){
if(ignoreSelectionChanges)

View File

@@ -67,7 +67,7 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowReque
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
setTitle(R.string.sk_follow_requests);
setTitle(R.string.follow_requests);
}
@Override
@@ -302,7 +302,7 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowReque
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
if (!rel.requested && !rel.followedBy && adapter != null) {
data.remove(item);
adapter.notifyItemRemoved(getLayoutPosition());
adapter.notifyItemRemoved(getBindingAdapterPosition());
} else {
rebind();
}

View File

@@ -1,107 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop {
private String nextMaxID;
private String accountId;
public FollowedHashtagsFragment() {
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
setTitle(R.string.sk_hashtags_you_follow);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetFollowedHashtags(offset==0 ? null : nextMaxID, null, count, null)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<Hashtag> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountId);
}
@Override
protected RecyclerView.Adapter getAdapter() {
return new HashtagsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull
@Override
public HashtagViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new HashtagViewHolder();
}
@Override
public void onBindViewHolder(@NonNull HashtagViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class HashtagViewHolder extends BindableViewHolder<Hashtag> implements UsableRecyclerView.Clickable{
private final TextView title;
public HashtagViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
}
@Override
public void onBind(Hashtag item) {
title.setText(item.name);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_number_symbol_24_regular), null, null, null);
}
@Override
public void onClick() {
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
}
}
}

View File

@@ -16,7 +16,6 @@ import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -118,7 +117,6 @@ public class HashtagTimelineFragment extends StatusListFragment{
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '));
}
private void onFabClick(View v){

View File

@@ -57,7 +57,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name);
setTitle(R.string.app_name);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);

View File

@@ -19,11 +19,9 @@ import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
@@ -45,9 +43,12 @@ import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -105,8 +106,6 @@ public class HomeTimelineFragment extends StatusListFragment{
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v->UiUtils.pickAccountForCompose(getActivity(), accountID));
updateToolbarLogo();
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
@@ -265,14 +264,18 @@ public class HomeTimelineFragment extends StatusListFragment{
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
outer:
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
if(filterPredicate.test(s)){
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
for(Filter filter:filters){
if(filter.matches(s)){
continue outer;
}
}
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none.
@@ -318,6 +321,9 @@ public class HomeTimelineFragment extends StatusListFragment{
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
toolbarLogo.setImageResource(R.drawable.logo);
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
// toolbarLogo =new TextView(getActivity());
// toolbarLogo.setText(getString(R.string.app_name).toLowerCase(Locale.getDefault()));
// toolbarLogo.setTextAppearance(R.style.app_title);
toolbarShowNewPostsBtn=new Button(getActivity());
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
@@ -345,9 +351,7 @@ public class HomeTimelineFragment extends StatusListFragment{
}
FrameLayout logoWrap=new FrameLayout(getActivity());
FrameLayout.LayoutParams logoParams=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER);
logoParams.setMargins(0, V.dp(2), 0, 0);
logoWrap.addView(toolbarLogo, logoParams);
logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
Toolbar toolbar=getToolbar();

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.media.MediaRouter;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
@@ -11,7 +12,6 @@ import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -69,7 +69,6 @@ public class ListTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID));
}
private void onFabClick(View v){

View File

@@ -1,9 +1,14 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -18,13 +23,17 @@ import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> implements ScrollableToTop {
@@ -48,7 +57,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
profileDisplayUsername=args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
setTitle(getString(R.string.lists_with_user, profileDisplayUsername));
// setHasOptionsMenu(true);
}
}
@@ -150,7 +159,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_text, list);
super(getActivity(), R.layout.item_list_timeline, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@@ -158,10 +167,8 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_community_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {

View File

@@ -14,7 +14,6 @@ import android.widget.FrameLayout;
import android.widget.LinearLayout;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
@@ -74,26 +73,15 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.follow_requests) {
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
return true;
} else if (item.getItemId() == R.id.clear_notifications) {
UiUtils.confirmDeleteNotification(getActivity(), accountID, null, ()->{
for (int i = 0; i < tabViews.length; i++) {
getFragmentForPage(i).reload();
}
});
return true;
}
return false;
if (item.getItemId() != R.id.follow_requests) return false;
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
return true;
}
@Override
@@ -121,7 +109,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new DiscoverPagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override

View File

@@ -2,8 +2,6 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -12,6 +10,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
@@ -79,9 +78,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
case FAVORITE -> getString(R.string.user_favorited);
case POLL -> getString(R.string.poll_ended);
};
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText, n, null) : null;
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null;
if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null);
if(titleItem!=null){
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem imgItem){
@@ -211,7 +210,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
public void removeNotification(Notification n){
private void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;

View File

@@ -10,7 +10,6 @@ import android.app.Activity;
import android.app.Fragment;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
@@ -19,8 +18,6 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ImageSpan;
@@ -164,11 +161,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private String getPrefilledText() {
return account == null || AccountSessionManager.getInstance().isSelf(accountID, account)
? null : '@'+account.acct+' ';
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
@@ -242,7 +234,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
pager.setOffscreenPageLimit(5);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -258,7 +249,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies;
case 2 -> R.string.sk_pinned_posts;
case 2 -> R.string.pinned_posts;
case 3 -> R.string.media;
case 4 -> R.string.profile_about;
default -> throw new IllegalStateException();
@@ -280,7 +271,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
cover.setOnClickListener(this::onCoverClick);
refreshLayout.setOnRefreshListener(this);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v->UiUtils.pickAccountForCompose(getActivity(), accountID, getPrefilledText()));
if(loaded){
bindHeaderView();
@@ -294,11 +284,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnLongClickListener(v->{
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
}
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
}
UiUtils.copyText(username, '@'+usernameString);
return true;
});
@@ -555,29 +548,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
if(isOwnProfile)
return;
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), mute);
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
if(relationship.following) {
MenuItem hideBoosts = menu.findItem(R.id.hide_boosts);
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
manageUserLists.setVisible(true);
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
}else {
menu.findItem(R.id.hide_boosts).setVisible(false);
manageUserLists.setVisible(false);
menu.findItem(R.id.manage_user_lists).setVisible(false);
}
if(!account.isLocal())
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
@@ -638,14 +620,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
}else if(id==R.id.followed_hashtags){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowedHashtagsFragment.class, args);
}else if(id==R.id.scheduled){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
}
return true;
}
@@ -684,7 +658,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
notifyButton.setSelected(relationship.notifying);
if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username));
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
@@ -957,7 +931,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
if(getPrefilledText() != null) args.putString("prefilledText", getPrefilledText());
if(!AccountSessionManager.getInstance().isSelf(accountID, account)){
args.putString("prefilledText", '@'+account.acct+' ');
}
Nav.go(getActivity(), ComposeFragment.class, args);
}

View File

@@ -1,171 +0,0 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetScheduledStatuses;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ScheduledStatusListFragment extends BaseStatusListFragment<ScheduledStatus> {
private String nextMaxID;
private ImageButton fab;
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
public ScheduledStatusListFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.sk_unsent_posts);
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
fab.setOnClickListener(v -> Nav.go(getActivity(), ComposeFragment.class, args));
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, args));
if (getArguments().getBoolean("hide_fab", false)) fab.setVisibility(View.GONE);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null);
}
@Override
protected void addAccountToKnown(ScheduledStatus s) {}
@Override
public void onItemClick(String id) {
final Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = getStatusByID(id);
Status status = scheduledStatus.toStatus();
args.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
args.putParcelable("editStatus", Parcels.wrap(status));
args.putString("sourceText", status.text);
args.putString("sourceSpoiler", status.spoilerText);
args.putBoolean("redraftStatus", true);
setResult(true, null);
// closing this scheduled status list if another status list is opened from compose fragment
Nav.goForResult(getActivity(), ComposeFragment.class, args, SCHEDULED_STATUS_LIST_OPENED, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result) {
if (reqCode == SCHEDULED_STATUS_LIST_OPENED && success && getActivity() != null) {
Nav.finish(this);
}
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetScheduledStatuses(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<ScheduledStatus> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountID);
}
// copied from StatusListFragment.java
@Subscribe
public void onScheduledStatusDeleted(ScheduledStatusDeletedEvent ev){
if(!ev.accountID.equals(accountID)) return;
ScheduledStatus status=getStatusByID(ev.id);
if(status==null) return;
removeStatus(status);
}
// copied from StatusListFragment.java
@Subscribe
public void onScheduledStatusCreated(ScheduledStatusCreatedEvent ev){
if(!ev.accountID.equals(accountID)) return;
prependItems(Collections.singletonList(ev.scheduledStatus), true);
scrollToTop();
}
// copied from StatusListFragment.java
protected void removeStatus(ScheduledStatus status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(status.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(status.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
// copied from StatusListFragment.java
protected ScheduledStatus getStatusByID(String id){
for(ScheduledStatus s:data){
if(s.id.equals(id)){
return s;
}
}
for(ScheduledStatus s:preloadedData){
if(s.id.equals(id)){
return s;
}
}
return null;
}
}

View File

@@ -9,21 +9,18 @@ import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.util.TypedValue;
import android.provider.Settings;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RadioButton;
@@ -36,7 +33,6 @@ 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.ColorPreference;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@@ -46,15 +42,12 @@ import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.function.Consumer;
@@ -64,8 +57,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageCache;
@@ -80,7 +71,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
private NotificationPolicyItem notificationPolicyItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private boolean needAppRestart;
private PushSubscription pushSubscription;
private ImageView themeTransitionWindowView;
@@ -94,8 +84,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
setTitle(R.string.settings);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
String instanceName = UiUtils.getInstanceName(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
@@ -108,70 +96,13 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new HeaderItem(R.string.settings_theme));
items.add(themeItem=new ThemeItem());
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
items.add(new ButtonItem(R.string.sk_settings_color_palette, R.drawable.ic_fluent_color_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.color_palettes);
popupMenu.getMenu().findItem(R.id.m3_color).setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S);
popupMenu.setOnMenuItemClickListener(SettingsFragment.this::onColorPreferenceClick);
b.setOnTouchListener(popupMenu.getDragToOpenListener());
b.setOnClickListener(v->popupMenu.show());
b.setText(switch(GlobalUserPreferences.color){
case MATERIAL3 -> R.string.sk_color_palette_material3;
case PINK -> R.string.sk_color_palette_pink;
case PURPLE -> R.string.sk_color_palette_purple;
case GREEN -> R.string.sk_color_palette_green;
case BLUE -> R.string.sk_color_palette_blue;
case BROWN -> R.string.sk_color_palette_brown;
case RED -> R.string.sk_color_palette_red;
case YELLOW -> R.string.sk_color_palette_yellow;
});
}));
items.add(new ButtonItem(R.string.sk_settings_publish_button_text, R.drawable.ic_fluent_send_24_regular, b->{
updatePublishText(b);
b.setOnClickListener(l->{
FrameLayout inputWrap = new FrameLayout(getContext());
EditText input = new EditText(getContext());
input.setHint(R.string.publish);
input.setText(GlobalUserPreferences.publishButtonText.trim());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(V.dp(16), V.dp(4), V.dp(16), V.dp(16));
input.setLayoutParams(params);
inputWrap.addView(input);
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(inputWrap)
.setPositiveButton(R.string.save, (d, which) -> {
GlobalUserPreferences.publishButtonText = input.getText().toString().trim();
GlobalUserPreferences.save();
updatePublishText(b);
})
.setNeutralButton(R.string.clear, (d, which) -> {
GlobalUserPreferences.publishButtonText = "";
GlobalUserPreferences.save();
updatePublishText(b);
})
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
});
}));
items.add(new SwitchItem(R.string.sk_settings_uniform_icon_for_notifications, R.drawable.ic_ntf_logo, GlobalUserPreferences.uniformNotificationIcon, i->{
GlobalUserPreferences.uniformNotificationIcon=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{
items.add(new SwitchItem(R.string.disable_marquee, R.drawable.ic_fluent_text_more_24_regular, GlobalUserPreferences.disableMarquee, i->{
GlobalUserPreferences.disableMarquee=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_reduce_motion, R.drawable.ic_fluent_star_emphasis_24_regular, GlobalUserPreferences.reduceMotion, i->{
GlobalUserPreferences.reduceMotion=i.checked;
GlobalUserPreferences.save();
}));
items.add(new ColorPicker());
items.add(new HeaderItem(R.string.settings_behavior));
items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{
GlobalUserPreferences.showFederatedTimeline=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
GlobalUserPreferences.playGifs=i.checked;
GlobalUserPreferences.save();
@@ -180,44 +111,25 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.useCustomTabs=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{
items.add(new SwitchItem(R.string.settings_show_interaction_counts, R.drawable.ic_fluent_number_row_24_regular, GlobalUserPreferences.showInteractionCounts, i->{
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(new SwitchItem(R.string.settings_always_reveal_content_warnings, R.drawable.ic_fluent_chat_warning_24_regular, GlobalUserPreferences.alwaysExpandContentWarnings, i->{
GlobalUserPreferences.alwaysExpandContentWarnings=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_tabs_disable_swipe, R.drawable.ic_fluent_swipe_right_24_regular, GlobalUserPreferences.disableSwipe, i->{
GlobalUserPreferences.disableSwipe=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_enable_delete_notifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, GlobalUserPreferences.enableDeleteNotifications, i->{
GlobalUserPreferences.enableDeleteNotifications=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{
GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled;
items.add(new SmallTextItem(getString(translationAvailable ?
R.string.sk_settings_translation_availability_note_available :
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
items.add(new HeaderItem(R.string.home_timeline));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
items.add(new SwitchItem(R.string.settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
items.add(new SwitchItem(R.string.settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
GlobalUserPreferences.showBoosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
items.add(new SwitchItem(R.string.settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
GlobalUserPreferences.loadNewPosts=i.checked;
GlobalUserPreferences.save();
}));
@@ -228,44 +140,23 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_alert_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new HeaderItem(R.string.settings_account));
items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_posting, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/preferences/other"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_filters, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/filters"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
items.add(new HeaderItem(R.string.settings_boring));
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new HeaderItem(instanceName));
items.add(new TextItem(R.string.sk_settings_rules, ()->{
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}, R.drawable.ic_fluent_task_list_ltr_24_regular));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
items.add(new HeaderItem(R.string.sk_settings_about));
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular));
items.add(new RedHeaderItem(R.string.settings_spicy));
if (GithubSelfUpdater.needSelfUpdating()) {
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
items.add(checkForUpdateItem);
}
items.add(new TextItem(R.string.settings_contribute_fork, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/LucasGGamerM/moshidon")));
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.sk_clear_recent_languages, ()->UiUtils.showConfirmationAlert(getActivity(), R.string.sk_clear_recent_languages, R.string.sk_confirm_clear_recent_languages, R.string.clear, ()->{
GlobalUserPreferences.recentLanguages.remove(accountID);
GlobalUserPreferences.save();
})));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
private void updatePublishText(Button btn) {
if (GlobalUserPreferences.publishButtonText.isBlank()) btn.setText(R.string.publish);
else btn.setText(GlobalUserPreferences.publishButtonText);
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
@Override
@@ -313,11 +204,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
}
if(needAppRestart){
Intent intent = Intent.makeRestartActivityTask(MastodonApp.context.getPackageManager().getLaunchIntentForPackage(MastodonApp.context.getPackageName()).getComponent());
MastodonApp.context.startActivity(intent);
Runtime.getRuntime().exit(0);
}
}
@Override
@@ -340,25 +226,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
restartActivityToApplyNewTheme();
}
private boolean onColorPreferenceClick(MenuItem item){
ColorPreference pref = null;
int id = item.getItemId();
if (id == R.id.m3_color) pref = ColorPreference.MATERIAL3;
else if (id == R.id.pink_color) pref = ColorPreference.PINK;
else if (id == R.id.purple_color) pref = ColorPreference.PURPLE;
else if (id == R.id.green_color) pref = ColorPreference.GREEN;
else if (id == R.id.blue_color) pref = ColorPreference.BLUE;
else if (id == R.id.brown_color) pref = ColorPreference.BROWN;
else if (id == R.id.red_color) pref = ColorPreference.RED;
else if (id == R.id.yellow_color) pref = ColorPreference.YELLOW;
if (pref == null) return false;
GlobalUserPreferences.color=pref;
private void onColorPreferenceClick(GlobalUserPreferences.ColorPreference color){
GlobalUserPreferences.color=color;
GlobalUserPreferences.save();
restartActivityToApplyNewTheme();
return true;
}
private void onTrueBlackThemeChanged(SwitchItem item){
@@ -420,7 +291,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
case FOLLOW -> subscription.alerts.follow=enabled;
case REBLOG -> subscription.alerts.reblog=enabled;
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
case STATUS -> subscription.alerts.status=enabled;
}
needUpdateNotificationSettings=true;
}
@@ -515,7 +385,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
Toast.makeText(getActivity(), R.string.sk_no_update_available, Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), R.string.no_update_available, Toast.LENGTH_SHORT).show();
}
}
@@ -530,10 +400,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.text=getString(text);
}
public HeaderItem(String text) {
this.text=text;
}
@Override
public int getViewType(){
return 0;
@@ -568,17 +434,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
public class ButtonItem extends Item{
private int text;
private int icon;
private Consumer<Button> buttonConsumer;
public ButtonItem(@StringRes int text, @DrawableRes int icon, Consumer<Button> buttonConsumer) {
this.text = text;
this.icon = icon;
this.buttonConsumer = buttonConsumer;
}
public class ColorPicker extends Item{
@Override
public int getViewType(){
return 8;
@@ -601,42 +457,19 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
private class SmallTextItem extends Item {
private String text;
public SmallTextItem(String text) {
this.text = text;
}
@Override
public int getViewType() {
return 9;
}
}
private class TextItem extends Item{
private String text;
private Runnable onClick;
private boolean loading;
private int icon;
public TextItem(@StringRes int text, Runnable onClick) {
this(text, onClick, false, 0);
this(text, onClick, false);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading) {
this(text, onClick, loading, 0);
}
public TextItem(@StringRes int text, Runnable onClick, @DrawableRes int icon) {
this(text, onClick, false, icon);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading, @DrawableRes int icon){
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
this.text=getString(text);
this.onClick=onClick;
this.loading=loading;
this.icon=icon;
}
@Override
@@ -692,8 +525,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
case 5 -> new HeaderViewHolder(true);
case 6 -> new FooterViewHolder();
case 7 -> new UpdateViewHolder();
case 8 -> new ButtonViewHolder();
case 9 -> new SmallTextViewHolder();
case 8 -> new ColorPickerViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@@ -822,24 +654,65 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
}
private class ButtonViewHolder extends BindableViewHolder<ButtonItem>{
private class ColorPickerViewHolder extends BindableViewHolder<ColorPicker>{
private final Button button;
private final PopupMenu popupMenu;
private final ImageView icon;
private final TextView text;
@SuppressLint("ClickableViewAccessibility")
public ButtonViewHolder(){
super(getActivity(), R.layout.item_settings_button, list);
text=findViewById(R.id.text);
public ColorPickerViewHolder(){
super(getActivity(), R.layout.item_settings_color_picker, list);
icon=findViewById(R.id.icon);
button=findViewById(R.id.button);
button=findViewById(R.id.color_picker_button);
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.color_picker);
popupMenu.setOnMenuItemClickListener(item->{
GlobalUserPreferences.ColorPreference pref;
int id=item.getItemId();
if(id==R.id.pink_color) {
pref = GlobalUserPreferences.ColorPreference.PINK;
onColorPreferenceClick(pref);
}
else if(id==R.id.purple_color) {
pref = GlobalUserPreferences.ColorPreference.PURPLE;
onColorPreferenceClick(pref);
}
else if(id==R.id.green_color) {
pref = GlobalUserPreferences.ColorPreference.GREEN;
onColorPreferenceClick(pref);
}
else if(id==R.id.blue_color) {
pref = GlobalUserPreferences.ColorPreference.BLUE;
onColorPreferenceClick(pref);
}
else if(id==R.id.orange_color) {
pref = GlobalUserPreferences.ColorPreference.ORANGE;
onColorPreferenceClick(pref);
}
else if(id==R.id.yellow_color) {
pref = GlobalUserPreferences.ColorPreference.YELLOW;
onColorPreferenceClick(pref);
}
else
return false;
return true;
});
// UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
button.setOnTouchListener(popupMenu.getDragToOpenListener());
button.setOnClickListener(v->popupMenu.show());
}
@Override
public void onBind(ButtonItem item){
text.setText(item.text);
icon.setImageResource(item.icon);
item.buttonConsumer.accept(button);
public void onBind(ColorPicker item){
icon.setImageResource(R.drawable.ic_color_theme_preference);
button.setText(switch(GlobalUserPreferences.color){
case PINK -> R.string.pink_color;
case PURPLE -> R.string.purple_color;
case GREEN -> R.string.green_color;
case BLUE -> R.string.blue_color;
case ORANGE -> R.string.orange_color;
case YELLOW -> R.string.yellow_color;
});
}
}
@@ -889,20 +762,17 @@ public class SettingsFragment extends MastodonToolbarFragment{
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
private final TextView text;
private final ProgressBar progress;
private final ImageView icon;
public TextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text = itemView.findViewById(R.id.text);
progress = itemView.findViewById(R.id.progress);
icon = itemView.findViewById(R.id.icon);
}
@Override
public void onBind(TextItem item){
text.setText(item.text);
progress.animate().alpha(item.loading ? 1 : 0);
if (item.icon != 0) icon.setImageDrawable(getActivity().getTheme().getDrawable(item.icon));
}
@Override
@@ -911,24 +781,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
private class SmallTextViewHolder extends BindableViewHolder<SmallTextItem> {
private final TextView text;
;
public SmallTextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text = itemView.findViewById(R.id.text);
}
@Override
public void onBind(SmallTextItem item){
text.setText(item.text);
text.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
}
}
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
private final TextView text;
public FooterViewHolder(){
@@ -979,10 +831,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
text.setText(getString(R.string.sk_update_available, info.version));
text.setText(getString(R.string.update_available, info.version));
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
}else{
text.setText(getString(R.string.sk_update_ready, info.version));
text.setText(getString(R.string.update_ready, info.version));
button.setText(R.string.install_update);
}
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){

View File

@@ -1,28 +1,20 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.content.res.Configuration;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogFragment;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V;
@@ -31,13 +23,12 @@ public class SplashFragment extends AppKitFragment{
private SizeListenerFrameLayout contentView;
private View artContainer, blueFill, greenFill;
private ViewPager2 pager;
private ViewGroup pagerDots;
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
private InterpolatingMotionEffect motionEffect;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
}
@Nullable
@@ -46,44 +37,15 @@ public class SplashFragment extends AppKitFragment{
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
artClouds=contentView.findViewById(R.id.art_clouds);
artPlaneElephant=contentView.findViewById(R.id.art_plane_elephant);
artRightHill=contentView.findViewById(R.id.art_right_hill);
artLeftHill=contentView.findViewById(R.id.art_left_hill);
artCenterHill=contentView.findViewById(R.id.art_center_hill);
pager=contentView.findViewById(R.id.pager);
pagerDots=contentView.findViewById(R.id.pager_dots);
pager.setAdapter(new PagerAdapter());
pager.setOffscreenPageLimit(3);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
for(int i=0;i<pagerDots.getChildCount();i++){
float alpha;
if(i==position){
alpha=0.3f+0.7f*(1f-positionOffset);
}else if(i==position+1){
alpha=0.3f+0.7f*positionOffset;
}else{
alpha=0.3f;
}
pagerDots.getChildAt(i).setAlpha(alpha);
}
float parallaxProgress=(position+positionOffset)/2f;
artClouds.setTranslationX(V.dp(-27)*(position>=1 ? 1f : positionOffset));
artPlaneElephant.setTranslationX(V.dp(101.55f)*parallaxProgress);
artLeftHill.setTranslationX(V.dp(-88)*parallaxProgress);
artLeftHill.setTranslationY(V.dp(24)*parallaxProgress);
artRightHill.setTranslationX(V.dp(-88)*parallaxProgress);
artRightHill.setTranslationY(V.dp(-24)*parallaxProgress);
artCenterHill.setTranslationX(V.dp(-40)*parallaxProgress);
}
});
artContainer=contentView.findViewById(R.id.art_container);
blueFill=contentView.findViewById(R.id.blue_fill);
greenFill=contentView.findViewById(R.id.green_fill);
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_clouds), V.dp(-5), V.dp(5), V.dp(-5), V.dp(5)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_right_hill), V.dp(-15), V.dp(25), V.dp(-10), V.dp(10)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_left_hill), V.dp(-25), V.dp(15), V.dp(-15), V.dp(15)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_center_hill), V.dp(-14), V.dp(14), V.dp(-5), V.dp(25)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_plane_elephant), V.dp(-20), V.dp(12), V.dp(-20), V.dp(12)));
contentView.setSizeListener(new SizeListenerFrameLayout.OnSizeChangedListener(){
@Override
@@ -104,16 +66,15 @@ public class SplashFragment extends AppKitFragment{
private void onButtonClick(View v){
Bundle extras=new Bundle();
boolean isSignup=v.getId()==R.id.btn_get_started;
extras.putBoolean("signup", isSignup);
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
extras.putBoolean("signup", v.getId()==R.id.btn_get_started);
Nav.go(getActivity(), InstanceCatalogFragment.class, extras);
}
private void updateArtSize(int w, int h){
float scale=w/(float)V.dp(360);
float scale=w/(float)V.dp(412);
artContainer.setScaleX(scale);
artContainer.setScaleY(scale);
blueFill.setScaleY(artContainer.getBottom()-V.dp(90));
blueFill.setScaleY(h/2f);
greenFill.setScaleY(h-artContainer.getBottom()+V.dp(90));
}
@@ -139,91 +100,15 @@ public class SplashFragment extends AppKitFragment{
return true;
}
private class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder>{
@NonNull
@Override
public PagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new PagerViewHolder(viewType);
}
@Override
public void onBindViewHolder(@NonNull PagerViewHolder holder, int position){}
@Override
public int getItemCount(){
return 3;
}
@Override
public int getItemViewType(int position){
return position;
}
@Override
protected void onShown(){
super.onShown();
motionEffect.activate();
}
private class PagerViewHolder extends RecyclerView.ViewHolder{
public PagerViewHolder(int page){
super(new LinearLayout(getActivity()));
LinearLayout ll=(LinearLayout) itemView;
ll.setOrientation(LinearLayout.VERTICAL);
int pad=V.dp(16);
ll.setPadding(pad, pad, pad, pad);
ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
TextView title=new TextView(getActivity());
title.setTextAppearance(R.style.m3_headline_medium);
title.setText(switch(page){
case 0 -> {
String src=getString(R.string.welcome_page1_title);
SpannableString ss=new SpannableString(src);
int start=src.indexOf("{logo}");
if(start!=-1){
LogoSpan span=new LogoSpan(getResources().getDrawable(R.drawable.splash_logo, getActivity().getTheme()));
ss.setSpan(span, start, start+6, 0);
}
yield ss;
}
case 1 -> getString(R.string.welcome_page2_title);
case 2 -> getString(R.string.welcome_page3_title);
default -> throw new IllegalStateException("Unexpected value: "+page);
});
title.setTextColor(0xFF17063B);
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(page==0 ? 46 : 36));
lp.bottomMargin=V.dp(page==0 ? 4 : 14);
ll.addView(title, lp);
TextView text=new TextView(getActivity());
text.setTextAppearance(R.style.m3_body_medium);
text.setText(switch(page){
case 0 -> R.string.welcome_page1_text;
case 1 -> R.string.welcome_page2_text;
case 2 -> R.string.welcome_page3_text;
default -> throw new IllegalStateException("Unexpected value: "+page);
});
text.setTextColor(0xFF17063B);
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
}
private class LogoSpan extends ReplacementSpan{
private final Drawable drawable;
private LogoSpan(Drawable drawable){
this.drawable=drawable;
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
return drawable.getIntrinsicWidth();
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
canvas.save();
canvas.translate(x, y-V.dp(20));
drawable.draw(canvas);
canvas.restore();
}
@Override
protected void onHidden(){
super.onHidden();
motionEffect.deactivate();
}
}

View File

@@ -54,7 +54,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -139,7 +139,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
action=getString(R.string.edit_multiple_changed);
}
}
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null, null));
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null));
}
return items;
}

View File

@@ -29,7 +29,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null);
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true);
}
@Override
@@ -177,7 +177,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
for(Status s:data){
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
s.update(ev);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
@@ -189,8 +189,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
}
}
for(Status s:preloadedData){
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
if(s.id.equals(ev.id)){
s.update(ev);
}
}
}

View File

@@ -5,6 +5,7 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
@@ -16,7 +17,6 @@ import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.Collections;
@@ -92,10 +92,16 @@ public class ThreadFragment extends StatusListFragment{
}
private List<Status> filterStatuses(List<Status> statuses){
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
return statuses.stream()
.filter(statusFilterPredicate)
.collect(Collectors.toList());
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
if(filters.isEmpty())
return statuses;
return statuses.stream().filter(status->{
for(Filter filter:filters){
if(filter.matches(status))
return false;
}
return true;
}).collect(Collectors.toList());
}
@Override

View File

@@ -23,7 +23,6 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
@@ -287,17 +286,13 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername())).setVisible(relationship.following);
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.lists_with_user, account.getDisplayUsername()));
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
if(relationship.following){
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
hideBoosts.setVisible(true);
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername()));
manageUserLists.setVisible(true);
}else{
hideBoosts.setVisible(false);
manageUserLists.setVisible(true);
}
MenuItem blockDomain=menu.findItem(R.id.block_domain);
if(!account.isLocal()){
@@ -374,12 +369,6 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.manage_user_lists){
final Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
}
return true;
}

View File

@@ -19,7 +19,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
@@ -62,7 +61,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
private final boolean noFederated = !GlobalUserPreferences.showFederatedTimeline;
private static final boolean noFederated = BuildConfig.BUILD_TYPE.equals("noFederatedRelease");
@Override
public void onCreate(Bundle savedInstanceState){
@@ -104,7 +103,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new DiscoverPagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
@@ -165,12 +163,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
if (noFederated && position > 0) position++;
tab.setText(switch(position){
case 0 -> R.string.local_timeline;
case 1 -> R.string.sk_federated_timeline;
case 1 -> R.string.federated_timeline;
case 2 -> R.string.hashtags;
case 3 -> R.string.posts;
case 4 -> R.string.news;
case 5 -> R.string.for_you;
case 6 -> R.string.sk_list_timelines;
case 6 -> R.string.list_timelines;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);

View File

@@ -75,7 +75,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null);
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
};
}

View File

@@ -1,8 +1,8 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -12,21 +12,19 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.ResendConfirmationEmail;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
@@ -37,50 +35,40 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends ToolbarFragment{
public class AccountActivationFragment extends AppKitFragment{
private String accountID;
private Button openEmailBtn, resendBtn;
private View contentView;
private Button btn, backBtn;
private View buttonBar;
private Handler uiHandler=new Handler(Looper.getMainLooper());
private Runnable pollRunnable=this::tryGetAccount;
private APIRequest currentRequest;
private Runnable resendTimer=this::updateResendTimer;
private long lastResendTime;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setTitle(R.string.confirm_email_title);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
lastResendTime=session.activationInfo!=null ? session.activationInfo.lastEmailConfirmationResend : 0;
}
@Nullable
@Override
public View onCreateContentView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_activation, container, false);
openEmailBtn=view.findViewById(R.id.btn_next);
openEmailBtn.setOnClickListener(this::onOpenEmailClick);
openEmailBtn.setOnLongClickListener(v->{
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
btn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
return true;
});
resendBtn=view.findViewById(R.id.btn_resend);
resendBtn.setOnClickListener(this::onResendClick);
TextView text=view.findViewById(R.id.subtitle);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
text.setText(getString(R.string.confirm_email_subtitle, session.activationInfo!=null ? session.activationInfo.email : "?"));
updateResendTimer();
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->onBackButtonClick());
contentView=view;
return view;
}
@@ -92,32 +80,14 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
@Override
protected boolean canGoBack(){
return true;
}
@Override
public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity()).show();
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
contentView.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
@@ -141,7 +111,7 @@ public class AccountActivationFragment extends ToolbarFragment{
}
}
private void onOpenEmailClick(View v){
private void onButtonClick(){
try{
startActivity(Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_EMAIL).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}catch(ActivityNotFoundException x){
@@ -149,21 +119,12 @@ public class AccountActivationFragment extends ToolbarFragment{
}
}
private void onResendClick(View v){
private void onBackButtonClick(){
new ResendConfirmationEmail(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
Toast.makeText(getActivity(), R.string.resent_email, Toast.LENGTH_SHORT).show();
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(session.activationInfo==null){
session.activationInfo=new AccountActivationInfo("?", System.currentTimeMillis());
}else{
session.activationInfo.lastEmailConfirmationResend=System.currentTimeMillis();
}
lastResendTime=session.activationInfo.lastEmailConfirmationResend;
AccountSessionManager.getInstance().writeAccountsFile();
updateResendTimer();
}
@Override
@@ -191,7 +152,7 @@ public class AccountActivationFragment extends ToolbarFragment{
AccountSessionManager mgr=AccountSessionManager.getInstance();
AccountSession session=mgr.getAccount(accountID);
mgr.removeAccount(accountID);
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, null);
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, true);
String newID=mgr.getLastActiveAccountID();
Bundle args=new Bundle();
args.putString("account", newID);
@@ -228,25 +189,4 @@ public class AccountActivationFragment extends ToolbarFragment{
})
.exec(accountID);
}
@SuppressLint("DefaultLocale")
private void updateResendTimer(){
long sinceResend=System.currentTimeMillis()-lastResendTime;
if(sinceResend>59_000L){
resendBtn.setText(R.string.resend);
resendBtn.setEnabled(true);
return;
}
int seconds=(int)((60_000L-sinceResend)/1000L);
resendBtn.setText(String.format("%s (%d)", getString(R.string.resend), seconds));
if(resendBtn.isEnabled())
resendBtn.setEnabled(false);
resendBtn.postDelayed(resendTimer, 500);
}
@Override
public void onDestroyView(){
super.onDestroyView();
resendBtn.removeCallbacks(resendTimer);
}
}

View File

@@ -1,255 +0,0 @@
package org.joinmastodon.android.fragments.onboarding;
import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.Space;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Objects;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class CustomWelcomeFragment extends InstanceCatalogFragment {
private View headerView;
public CustomWelcomeFragment() {
super(R.layout.fragment_welcome_custom, 1);
}
@Override
public void onAttach(Context context){
super.onAttach(context);
setRefreshEnabled(false);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
dataLoaded();
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if (!canGoBack()) {
ImageView toolbarLogo=new ImageView(getActivity());
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
toolbarLogo.setImageResource(R.drawable.logo);
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
FrameLayout logoWrap=new FrameLayout(getActivity());
FrameLayout.LayoutParams logoParams=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER);
logoParams.setMargins(0, V.dp(2), 0, 0);
logoWrap.addView(toolbarLogo, logoParams);
getToolbar().addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
} else {
setTitle(R.string.add_account);
}
}
@Override
protected void proceedWithAuthOrSignup(Instance instance) {
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
}
@Override
protected void updateFilteredList(){
boolean addFakeInstance = currentSearchQuery.length()>0 && currentSearchQuery.matches("^\\S+\\.[^\\.]+$");
if(addFakeInstance){
fakeInstance.domain=fakeInstance.normalizedDomain=currentSearchQuery;
fakeInstance.description=getString(R.string.loading_instance);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof InstanceViewHolder ivh){
ivh.rebind();
}
}
if(filteredData.isEmpty()){
filteredData.add(fakeInstance);
adapter.notifyItemInserted(0);
}
}
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
if(currentSearchQuery.length()>0){
boolean foundExactMatch=false;
for(CatalogInstance inst:data){
if(inst.normalizedDomain.contains(currentSearchQuery)){
filteredData.add(inst);
if(inst.normalizedDomain.equals(currentSearchQuery))
foundExactMatch=true;
}
}
if(!foundExactMatch && addFakeInstance) {
filteredData.add(0, fakeInstance);
adapter.notifyItemChanged(0);
}
}
UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals);
for(int i=0;i<list.getChildCount();i++){
list.getChildAt(i).invalidateOutline();
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
list.setItemAnimator(new BetterItemAnimator());
}
@Override
protected void doLoadData(int offset, int count) {}
@Override
protected RecyclerView.Adapter getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_welcome_custom, list, false);
searchEdit=headerView.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
headerView.findViewById(R.id.more).setVisibility(View.GONE);
headerView.findViewById(R.id.visibility).setVisibility(View.GONE);
headerView.findViewById(R.id.separator).setVisibility(View.GONE);
headerView.findViewById(R.id.timestamp).setVisibility(View.GONE);
((TextView) headerView.findViewById(R.id.username)).setText(R.string.sk_app_username);
((TextView) headerView.findViewById(R.id.name)).setText(R.string.sk_app_name);
((ImageView) headerView.findViewById(R.id.avatar)).setImageDrawable(getActivity().getDrawable(R.mipmap.ic_launcher));
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(this);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
nextButton.setEnabled(false);
chosenInstance = null;
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){}
});
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
View spacer = new Space(getActivity());
spacer.setMinimumHeight(V.dp(8));
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(spacer));
return mergeAdapter;
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder> {
public InstancesAdapter(){
super(imgLoader);
}
@NonNull
@Override
public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new InstanceViewHolder();
}
@Override
public void onBindViewHolder(InstanceViewHolder holder, int position){
holder.bind(filteredData.get(position));
chosenInstance = filteredData.get(position);
if (chosenInstance != fakeInstance) nextButton.setEnabled(true);
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return filteredData.size();
}
@Override
public int getItemViewType(int position){
return -1;
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description, userCount, lang;
private final RadioButton radioButton;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_custom, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);
lang=findViewById(R.id.lang);
radioButton=findViewById(R.id.radiobtn);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
}
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
if (item == fakeInstance) {
userCount.setVisibility(View.GONE);
lang.setVisibility(View.GONE);
} else {
userCount.setVisibility(View.VISIBLE);
lang.setVisibility(View.VISIBLE);
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
}
radioButton.setChecked(chosenInstance==item);
radioButton.setVisibility(View.GONE);
}
@Override
public void onClick(){
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
if(holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
}
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
onNextClick(null);
}
}
}

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
@@ -16,7 +15,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.Jsoup;
@@ -35,7 +33,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
@@ -49,7 +46,7 @@ import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
public class GoogleMadeMeAddThisFragment extends AppKitFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@@ -63,7 +60,6 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.privacy_policy_title);
}
@Override
@@ -86,24 +82,37 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text);
text.setText(R.string.privacy_policy_subtitle);
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
title.setText(R.string.privacy_policy_title);
subtitle.setText(R.string.privacy_policy_subtitle);
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
list.setSelector(null);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.getChildViewHolder(view) instanceof ItemViewHolder){
outRect.left=outRect.right=V.dp(18.5f);
outRect.top=V.dp(16);
}
}
});
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
return view;
}
@@ -111,15 +120,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
protected void onButtonClick(){
@@ -191,17 +192,24 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
}
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView title;
private final TextView domain, title;
private final ImageView favicon;
public ItemViewHolder(){
super(getActivity(), R.layout.item_privacy_policy_link, list);
domain=findViewById(R.id.domain);
title=findViewById(R.id.title);
title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
favicon=findViewById(R.id.favicon);
itemView.setOutlineProvider(OutlineProviders.roundedRect(10));
itemView.setClipToOutline(true);
}
@Override
public void onBind(Item item){
domain.setText(item.domain);
title.setText(item.title);
ViewImageLoader.load(favicon, null, new UrlImageLoaderRequest(item.faviconUrl));
}
@Override

View File

@@ -2,30 +2,46 @@ package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
@@ -43,53 +59,317 @@ import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
protected RecyclerView.Adapter adapter;
protected MergeRecyclerAdapter mergeAdapter;
protected CatalogInstance chosenInstance;
protected Button nextButton;
protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery;
protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar;
protected List<CatalogInstance> filteredData=new ArrayList<>();
protected GetInstance loadingInstanceRequest;
protected Call loadingInstanceRedirectRequest;
protected ProgressDialog instanceProgressDialog;
protected HashMap<String, String> redirects=new HashMap<>();
protected HashMap<String, String> redirectsInverse=new HashMap<>();
protected boolean isSignup;
protected CatalogInstance fakeInstance=new CatalogInstance();
public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
private InstancesAdapter adapter;
private MergeRecyclerAdapter mergeAdapter;
private View headerView;
private CatalogInstance chosenInstance;
private List<CatalogInstance> filteredData=new ArrayList<>();
private Button nextButton;
private MastodonAPIRequest<?> getCategoriesRequest;
private EditText searchEdit;
private TabLayout categoriesList;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
private String currentSearchQuery;
private String currentCategory="all";
private List<CatalogCategory> categories=new ArrayList<>();
private String loadingInstanceDomain;
private GetInstance loadingInstanceRequest;
private Call loadingInstanceRedirectRequest;
private HashMap<String, Instance> instancesCache=new HashMap<>();
private ProgressDialog instanceProgressDialog;
private View buttonBar;
private HashMap<String, String> redirects=new HashMap<>(), redirectsInverse=new HashMap<>();
private boolean isSignup;
private static final double DUNBAR=Math.log(800);
public InstanceCatalogFragment(int layout, int perPage){
super(layout, perPage);
public InstanceCatalogFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
isSignup=getArguments() != null && getArguments().getBoolean("signup");
isSignup=getArguments().getBoolean("signup");
}
protected abstract void proceedWithAuthOrSignup(Instance instance);
@Override
public void onAttach(Context context){
super.onAttach(context);
setRefreshEnabled(false);
loadData();
}
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetCatalogInstances(null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
if(getActivity()==null)
return;
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
for(List<CatalogInstance> group:byLang.values()){
Collections.sort(group, (a, b)->{
double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers));
double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers));
return Double.compare(aa, bb);
});
}
// get the list of user-configured system languages
List<String> userLangs;
if(Build.VERSION.SDK_INT<24){
userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage());
}else{
LocaleList ll=getResources().getConfiguration().getLocales();
userLangs=new ArrayList<>(ll.size());
for(int i=0;i<ll.size();i++){
userLangs.add(ll.get(i).getLanguage());
}
}
// add instances in preferred languages to the top of the list, in the order of preference
ArrayList<CatalogInstance> sortedList=new ArrayList<>();
for(String lang:userLangs){
List<CatalogInstance> langInstances=byLang.remove(lang);
if(langInstances!=null){
sortedList.addAll(langInstances);
}
}
// sort the remaining language groups by aggregate lastWeekUsers
class InstanceGroup{
public int activeUsers;
public List<CatalogInstance> instances;
}
byLang.values().stream().map(il->{
InstanceGroup group=new InstanceGroup();
group.instances=il;
for(CatalogInstance instance:il){
group.activeUsers+=instance.lastWeekUsers;
}
return group;
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
onDataLoaded(sortedList, false);
updateFilteredList();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
onDataLoaded(Collections.emptyList(), false);
}
})
.execNoAuth("");
getCategoriesRequest=new GetCatalogCategories(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogCategory> result){
getCategoriesRequest=null;
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
updateCategories();
}
@Override
public void onError(ErrorResponse error){
getCategoriesRequest=null;
error.showToast(getActivity());
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
updateCategories();
}
})
.execNoAuth("");
}
private void updateCategories(){
categoriesList.removeAllTabs();
for(CatalogCategory cat:categories){
int titleRes=getTitleForCategory(cat.category);
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
emoji.setImageResource(getEmojiForCategory(cat.category));
categoriesList.addTab(tab);
}
}
@Override
public void onDestroy(){
super.onDestroy();
if(getCategoriesRequest!=null)
getCategoriesRequest.cancel();
}
@Override
protected RecyclerView.Adapter getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
searchEdit=headerView.findViewById(R.id.search_edit);
categoriesList=headerView.findViewById(R.id.categories_list);
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
CatalogCategory category=categories.get(tab.getPosition());
currentCategory=category.category;
updateFilteredList();
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
}
});
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
}
});
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setEnabled(chosenInstance!=null);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
buttonBar=view.findViewById(R.id.button_bar);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
private void onNextClick(View v){
String domain=chosenInstance.domain;
Instance instance=instancesCache.get(domain);
if(instance!=null){
proceedWithAuthOrSignup(instance);
}else{
showProgressDialog();
if(!domain.equals(loadingInstanceDomain)){
loadInstanceInfo(domain, false);
}
}
}
private void proceedWithAuthOrSignup(Instance instance){
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(isSignup){
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}else{
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
}
}
// private String getEmojiForCategory(String category){
// return switch(category){
// case "all" -> "💬";
// case "academia" -> "📚";
// case "activism" -> "✊";
// case "food" -> "🍕";
// case "furry" -> "🦁";
// case "games" -> "🕹";
// case "general" -> "🐘";
// case "journalism" -> "📰";
// case "lgbt" -> "🏳️‍🌈";
// case "regional" -> "📍";
// case "art" -> "🎨";
// case "music" -> "🎼";
// case "tech" -> "📱";
// default -> "❓";
// };
// }
private int getEmojiForCategory(String category){
return switch(category){
case "all" -> R.drawable.ic_category_all;
case "academia" -> R.drawable.ic_category_academia;
case "activism" -> R.drawable.ic_category_activism;
case "food" -> R.drawable.ic_category_food;
case "furry" -> R.drawable.ic_category_furry;
case "games" -> R.drawable.ic_category_games;
case "general" -> R.drawable.ic_category_general;
case "journalism" -> R.drawable.ic_category_journalism;
case "lgbt" -> R.drawable.ic_category_lgbt;
case "regional" -> R.drawable.ic_category_regional;
case "art" -> R.drawable.ic_category_art;
case "music" -> R.drawable.ic_category_music;
case "tech" -> R.drawable.ic_category_tech;
default -> R.drawable.ic_category_unknown;
};
}
private int getTitleForCategory(String category){
return switch(category){
case "all" -> R.string.category_all;
case "academia" -> R.string.category_academia;
case "activism" -> R.string.category_activism;
case "food" -> R.string.category_food;
case "furry" -> R.string.category_furry;
case "games" -> R.string.category_games;
case "general" -> R.string.category_general;
case "journalism" -> R.string.category_journalism;
case "lgbt" -> R.string.category_lgbt;
case "regional" -> R.string.category_regional;
case "art" -> R.string.category_art;
case "music" -> R.string.category_music;
case "tech" -> R.string.category_tech;
default -> 0;
};
}
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
@@ -105,73 +385,60 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return true;
}
protected void onSearchChangedDebounced(){
private void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
updateFilteredList();
loadInstanceInfo(currentSearchQuery, false);
}
protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
for(List<CatalogInstance> group:byLang.values()){
Collections.sort(group, (a, b)->{
double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers));
double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers));
return Double.compare(aa, bb);
});
}
// get the list of user-configured system languages
List<String> userLangs;
if(Build.VERSION.SDK_INT<24){
userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage());
}else{
LocaleList ll=getResources().getConfiguration().getLocales();
userLangs=new ArrayList<>(ll.size());
for(int i=0;i<ll.size();i++){
userLangs.add(ll.get(i).getLanguage());
private void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
for(CatalogInstance instance:data){
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
filteredData.add(instance);
}
}
}
// add instances in preferred languages to the top of the list, in the order of preference
ArrayList<CatalogInstance> sortedList=new ArrayList<>();
for(String lang:userLangs){
List<CatalogInstance> langInstances=byLang.remove(lang);
if(langInstances!=null){
sortedList.addAll(langInstances);
DiffUtil.calculateDiff(new DiffUtil.Callback(){
@Override
public int getOldListSize(){
return prevData.size();
}
}
// sort the remaining language groups by aggregate lastWeekUsers
class InstanceGroup{
public int activeUsers;
public List<CatalogInstance> instances;
}
byLang.values().stream().map(il->{
InstanceGroup group=new InstanceGroup();
group.instances=il;
for(CatalogInstance instance:il){
group.activeUsers+=instance.lastWeekUsers;
@Override
public int getNewListSize(){
return filteredData.size();
}
return group;
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
return sortedList;
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
}).dispatchUpdatesTo(adapter);
}
protected abstract void updateFilteredList();
protected void showProgressDialog(){
private void showProgressDialog(){
instanceProgressDialog=new ProgressDialog(getActivity());
instanceProgressDialog.setMessage(getString(R.string.loading_instance));
instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo());
instanceProgressDialog.show();
}
protected String normalizeInstanceDomain(String _domain){
private String normalizeInstanceDomain(String _domain){
if(TextUtils.isEmpty(_domain))
return null;
if(_domain.contains(":")){
try{
_domain=Uri.parse(_domain).getAuthority();
}catch(Exception ignore){
}
}catch(Exception ignore){}
if(TextUtils.isEmpty(_domain))
return null;
}
@@ -186,14 +453,12 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return domain;
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
if(TextUtils.isEmpty(_domain))
return;
private void loadInstanceInfo(String _domain, boolean isFromRedirect){
String domain=normalizeInstanceDomain(_domain);
Instance cachedInstance=instancesCache.get(domain);
if(cachedInstance!=null){
for(CatalogInstance ci : filteredData){
if(ci.domain.equals(domain) && ci!=fakeInstance)
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain))
return;
}
CatalogInstance ci=cachedInstance.toCatalogInstance();
@@ -211,57 +476,44 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceDomain=domain;
loadingInstanceRequest=new GetInstance();
loadingInstanceRequest.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
loadingInstanceRequest=null;
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain) && ci!=fakeInstance){
found=true;
break;
@Override
public void onSuccess(Instance result){
loadingInstanceRequest=null;
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain)){
found=true;
break;
}
}
if(!found){
CatalogInstance ci=result.toCatalogInstance();
filteredData.add(0, ci);
adapter.notifyItemInserted(0);
}
}
}
if(!found){
CatalogInstance ci=result.toCatalogInstance();
if(filteredData.size()==1 && filteredData.get(0)==fakeInstance){
filteredData.set(0, ci);
adapter.notifyItemChanged(0);
}else{
filteredData.add(0, ci);
adapter.notifyItemInserted(0);
}
}
}
}
@Override
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
return;
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
ivh.rebind();
@Override
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
return;
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
}
}
}
}).execNoAuth(domain);
}).execNoAuth(domain);
}
private void cancelLoadingInstanceInfo(){
@@ -332,7 +584,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
InputSource source=new InputSource(response.body().charStream());
Document doc=DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source);
NodeList list=doc.getElementsByTagName("Link");
for(int i=0; i<list.getLength(); i++){
for(int i=0;i<list.getLength();i++){
if(list.item(i) instanceof Element el){
String template=el.getAttribute("template");
if("lrdd".equals(el.getAttribute("rel")) && !TextUtils.isEmpty(template) && template.contains("{uri}")){
@@ -364,26 +616,78 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setEnabled(chosenInstance!=null);
buttonBar=view.findViewById(R.id.button_bar);
setRefreshEnabled(false);
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
public InstancesAdapter(){
super(imgLoader);
}
@NonNull
@Override
public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new InstanceViewHolder();
}
@Override
public void onBindViewHolder(InstanceViewHolder holder, int position){
holder.bind(filteredData.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return filteredData.size();
}
@Override
public int getItemViewType(int position){
return -1;
}
}
protected void onNextClick(View v){
String domain=chosenInstance.domain;
Instance instance=instancesCache.get(domain);
if(instance!=null){
proceedWithAuthOrSignup(instance);
}else{
showProgressDialog();
if(!domain.equals(loadingInstanceDomain)){
loadInstanceInfo(domain, false);
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description, userCount, lang;
private final RadioButton radioButton;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_catalog, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);
lang=findViewById(R.id.lang);
radioButton=findViewById(R.id.radiobtn);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
}
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
radioButton.setChecked(chosenInstance==item);
}
@Override
public void onClick(){
if(chosenInstance==item)
return;
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
if(holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
}
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
}
}
}

View File

@@ -1,713 +0,0 @@
package org.joinmastodon.android.fragments.onboarding;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{
private MastodonAPIRequest<?> getCategoriesRequest;
private String currentCategory="all";
private List<CatalogCategory> categories=new ArrayList<>();
private View topBar;
private List<String> languages=Collections.emptyList();
private PopupMenu langFilterMenu, speedFilterMenu;
private SignupSpeedFilter currentSignupSpeedFilter=SignupSpeedFilter.INSTANT;
private String currentLanguage=null;
private boolean searchQueryMode;
private LinearLayout filtersWrap;
private HorizontalScrollView filtersScroll;
private ImageButton backBtn, clearSearchBtn;
private View focusThing;
private FilterChipView categoryGeneral, categorySpecialInterests;
private List<FilterChipView> regionalFilters;
private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice;
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@Override
public void onAttach(Context context){
super.onAttach(context);
setRefreshEnabled(false);
setRetainInstance(true);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetCatalogInstances(null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
if(getActivity()==null)
return;
onDataLoaded(sortInstances(result), false);
if(langFilterMenu!=null){
Menu menu=langFilterMenu.getMenu();
menu.clear();
menu.add(0, 0, 0, R.string.server_filter_any_language);
languages=result.stream().map(i->i.language).distinct().filter(s->s.length()>0).sorted().collect(Collectors.toList());
int i=1;
for(String lang:languages){
Locale locale=Locale.forLanguageTag(lang);
String name=locale.getDisplayLanguage(locale);
if(name.equals(lang))
name=lang.toUpperCase();
else
name=name.substring(0, 1).toUpperCase()+name.substring(1);
menu.add(0, i, 0, name);
i++;
}
}
updateFilteredList();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
onDataLoaded(Collections.emptyList(), false);
}
})
.execNoAuth("");
getCategoriesRequest=new GetCatalogCategories(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogCategory> result){
getCategoriesRequest=null;
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
updateCategories();
}
@Override
public void onError(ErrorResponse error){
getCategoriesRequest=null;
error.showToast(getActivity());
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
updateCategories();
}
})
.execNoAuth("");
}
private void updateCategories(){
// categoriesList.removeAllTabs();
// for(CatalogCategory cat:categories){
// int titleRes=getTitleForCategory(cat.category);
// TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
// ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
// emoji.setImageResource(getEmojiForCategory(cat.category));
// categoriesList.addTab(tab);
// }
}
@Override
public void onDestroy(){
super.onDestroy();
if(getCategoriesRequest!=null)
getCategoriesRequest.cancel();
}
@Override
protected RecyclerView.Adapter getAdapter(){
View headerView=new View(getActivity());
headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
backBtn=view.findViewById(R.id.btn_back);
backBtn.setOnClickListener(v->{
if(searchQueryMode){
setSearchQueryMode(false);
}else{
Nav.finish(this);
}
});
clearSearchBtn=view.findViewById(R.id.clear);
clearSearchBtn.setOnClickListener(v->searchEdit.setText(""));
nextButton.setEnabled(true);
list.setItemAnimator(new BetterItemAnimator());
setStatusBarColor(0);
topBar=view.findViewById(R.id.top_bar);
LayerDrawable topBg=(LayerDrawable) topBar.getBackground().mutate();
topBar.setBackground(topBg);
Drawable topOverlay=topBg.findDrawableByLayerId(R.id.color_overlay);
topOverlay.setAlpha(0);
LayerDrawable btmBg=(LayerDrawable) buttonBar.getBackground().mutate();
buttonBar.setBackground(btmBg);
Drawable btmOverlay=btmBg.findDrawableByLayerId(R.id.color_overlay);
btmOverlay.setAlpha(0);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
private boolean isAtTop=true;
private Animator currentPanelsAnim;
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
if(newAtTop!=isAtTop){
isAtTop=newAtTop;
if(currentPanelsAnim!=null)
currentPanelsAnim.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(topOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofInt(btmOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofFloat(topBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)),
ObjectAnimator.ofFloat(buttonBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3))
);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentPanelsAnim=null;
}
});
set.start();
currentPanelsAnim=set;
}
}
});
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
if((clearSearchBtn.getVisibility()==View.VISIBLE)!=(s.length()>0)){
clearSearchBtn.setVisibility(s.length()>0 ? View.VISIBLE : View.GONE);
}
}
});
searchEdit.setOnFocusChangeListener((v, hasFocus)->{
if(hasFocus && !searchQueryMode){
setSearchQueryMode(true);
}
});
FilterChipView langFilter=new FilterChipView(getActivity());
langFilter.setDrawableEnd(R.drawable.ic_baseline_arrow_drop_down_18);
if(currentLanguage==null){
langFilter.setText(R.string.server_filter_any_language);
}else{
Locale locale=Locale.forLanguageTag(currentLanguage);
langFilter.setText(locale.getDisplayLanguage(locale));
langFilter.setSelected(true);
}
langFilterMenu=new PopupMenu(getContext(), langFilter);
langFilter.setOnTouchListener(langFilterMenu.getDragToOpenListener());
langFilter.setOnClickListener(v->langFilterMenu.show());
filtersWrap=view.findViewById(R.id.filters_container);
filtersScroll=view.findViewById(R.id.filters_scroll);
filtersWrap.addView(langFilter, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
FilterChipView speedFilter=new FilterChipView(getActivity());
speedFilter.setDrawableEnd(R.drawable.ic_baseline_arrow_drop_down_18);
speedFilterMenu=new PopupMenu(getContext(), speedFilter);
speedFilterMenu.getMenu().add(0, 0, 0, R.string.server_filter_any_signup_speed);
speedFilterMenu.getMenu().add(0, 1, 0, R.string.server_filter_instant_signup);
speedFilterMenu.getMenu().add(0, 2, 0, R.string.server_filter_manual_review);
speedFilter.setOnTouchListener(speedFilterMenu.getDragToOpenListener());
speedFilter.setOnClickListener(v->speedFilterMenu.show());
speedFilter.setText(switch(currentSignupSpeedFilter){
case ANY -> R.string.server_filter_any_signup_speed;
case INSTANT -> R.string.server_filter_instant_signup;
case REVIEWED -> R.string.server_filter_manual_review;
});
speedFilter.setSelected(currentSignupSpeedFilter!=SignupSpeedFilter.ANY);
filtersWrap.addView(speedFilter, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
speedFilterMenu.setOnMenuItemClickListener(item->{
speedFilter.setText(item.getTitle());
speedFilter.setSelected(item.getItemId()>0);
currentSignupSpeedFilter=SignupSpeedFilter.values()[item.getItemId()];
updateFilteredList();
return true;
});
langFilterMenu.setOnMenuItemClickListener(item->{
langFilter.setText(item.getTitle());
langFilter.setSelected(item.getItemId()>0);
currentLanguage=item.getItemId()==0 ? null : languages.get(item.getItemId()-1);
updateFilteredList();
return true;
});
View divider=new View(getActivity());
divider.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
filtersWrap.addView(divider, new LinearLayout.LayoutParams(V.dp(.5f), ViewGroup.LayoutParams.MATCH_PARENT));
categoryGeneral=new FilterChipView(getActivity());
categoryGeneral.setText(R.string.category_general);
categoryGeneral.setTag(CategoryChoice.GENERAL);
categoryGeneral.setOnClickListener(this::onCategoryFilterClick);
categoryGeneral.setSelected(categoryChoice==CategoryChoice.GENERAL);
filtersWrap.addView(categoryGeneral, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
categorySpecialInterests=new FilterChipView(getActivity());
categorySpecialInterests.setText(R.string.category_special_interests);
categorySpecialInterests.setTag(CategoryChoice.SPECIAL);
categorySpecialInterests.setOnClickListener(this::onCategoryFilterClick);
categorySpecialInterests.setSelected(categoryChoice==CategoryChoice.SPECIAL);
filtersWrap.addView(categorySpecialInterests, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
regionalFilters=Arrays.stream(CatalogInstance.Region.values()).map(r->{
FilterChipView fv=new FilterChipView(getActivity());
fv.setTag(r);
fv.setText(switch(r){
case EUROPE -> R.string.server_filter_region_europe;
case NORTH_AMERICA -> R.string.server_filter_region_north_america;
case SOUTH_AMERICA -> R.string.server_filter_region_south_america;
case AFRICA -> R.string.server_filter_region_africa;
case ASIA -> R.string.server_filter_region_asia;
case OCEANIA -> R.string.server_filter_region_oceania;
});
fv.setSelected(r==chosenRegion);
fv.setOnClickListener(this::onRegionFilterClick);
filtersWrap.addView(fv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
return fv;
}).collect(Collectors.toList());
focusThing=view.findViewById(R.id.focus_thing);
focusThing.requestFocus();
}
private void onRegionFilterClick(View v){
CatalogInstance.Region r=(CatalogInstance.Region) v.getTag();
if(chosenRegion==r){
chosenRegion=null;
v.setSelected(false);
}else{
if(chosenRegion!=null)
filtersWrap.findViewWithTag(chosenRegion).setSelected(false);
chosenRegion=r;
v.setSelected(true);
}
updateFilteredList();
}
private void onCategoryFilterClick(View v){
CategoryChoice c=(CategoryChoice) v.getTag();
if(categoryChoice==c){
categoryChoice=null;
v.setSelected(false);
}else{
if(categoryChoice!=null)
filtersWrap.findViewWithTag(categoryChoice).setSelected(false);
categoryChoice=c;
v.setSelected(true);
}
updateFilteredList();
}
@Override
protected void onNextClick(View v){
if(chosenInstance==null){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
}
super.onNextClick(v);
}
@Override
protected void proceedWithAuthOrSignup(Instance instance){
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
// private String getEmojiForCategory(String category){
// return switch(category){
// case "all" -> "💬";
// case "academia" -> "📚";
// case "activism" -> "✊";
// case "food" -> "🍕";
// case "furry" -> "🦁";
// case "games" -> "🕹";
// case "general" -> "🐘";
// case "journalism" -> "📰";
// case "lgbt" -> "🏳️‍🌈";
// case "regional" -> "📍";
// case "art" -> "🎨";
// case "music" -> "🎼";
// case "tech" -> "📱";
// default -> "❓";
// };
// }
private int getEmojiForCategory(String category){
return switch(category){
case "all" -> R.drawable.ic_category_all;
case "academia" -> R.drawable.ic_category_academia;
case "activism" -> R.drawable.ic_category_activism;
case "food" -> R.drawable.ic_category_food;
case "furry" -> R.drawable.ic_category_furry;
case "games" -> R.drawable.ic_category_games;
case "general" -> R.drawable.ic_category_general;
case "journalism" -> R.drawable.ic_category_journalism;
case "lgbt" -> R.drawable.ic_category_lgbt;
case "regional" -> R.drawable.ic_category_regional;
case "art" -> R.drawable.ic_category_art;
case "music" -> R.drawable.ic_category_music;
case "tech" -> R.drawable.ic_category_tech;
default -> R.drawable.ic_category_unknown;
};
}
private int getTitleForCategory(String category){
return switch(category){
case "all" -> R.string.category_all;
case "academia" -> R.string.category_academia;
case "activism" -> R.string.category_activism;
case "food" -> R.string.category_food;
case "furry" -> R.string.category_furry;
case "games" -> R.string.category_games;
case "general" -> R.string.category_general;
case "journalism" -> R.string.category_journalism;
case "lgbt" -> R.string.category_lgbt;
case "regional" -> R.string.category_regional;
case "art" -> R.string.category_art;
case "music" -> R.string.category_music;
case "tech" -> R.string.category_tech;
default -> 0;
};
}
@Override
protected void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){
for(CatalogInstance instance:data){
if(instance.domain.contains(currentSearchQuery)){
filteredData.add(instance);
}
}
}
}else{
for(CatalogInstance instance:data){
if(categoryChoice==null || categoryChoice.matches(instance.category)){
if(chosenRegion==null || instance.region==chosenRegion){
boolean signupSpeedMatches=switch(currentSignupSpeedFilter){
case ANY -> true;
case INSTANT -> !instance.approvalRequired;
case REVIEWED -> instance.approvalRequired;
};
if(signupSpeedMatches){
if(currentLanguage==null || instance.languages.contains(currentLanguage)){
filteredData.add(instance);
}
}
}
}
}
}
DiffUtil.calculateDiff(new DiffUtil.Callback(){
@Override
public int getOldListSize(){
return prevData.size();
}
@Override
public int getNewListSize(){
return filteredData.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
}
}).dispatchUpdatesTo(adapter);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
topBar.setPadding(0, insets.getSystemWindowInsetTop(), 0, 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
@Override
public boolean onBackPressed(){
if(searchQueryMode){
setSearchQueryMode(false);
return true;
}
return false;
}
private void setSearchQueryMode(boolean enabled){
searchQueryMode=enabled;
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams();
if(searchQueryMode){
filtersScroll.setVisibility(View.GONE);
lp.removeRule(RelativeLayout.END_OF);
backBtn.setScaleX(0.83333333f);
backBtn.setScaleY(0.83333333f);
backBtn.setTranslationX(V.dp(8));
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0));
}else{
filtersScroll.setVisibility(View.VISIBLE);
focusThing.requestFocus();
searchEdit.setText("");
lp.addRule(RelativeLayout.END_OF, R.id.btn_back);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
backBtn.setScaleX(1);
backBtn.setScaleY(1);
backBtn.setTranslationX(0);
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant)));
}
updateFilteredList();
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder> implements ImageLoaderRecyclerAdapter{
public InstancesAdapter(){
super(imgLoader);
}
@NonNull
@Override
public InstanceCatalogSignupFragment.InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new InstanceCatalogSignupFragment.InstanceViewHolder();
}
@Override
public void onBindViewHolder(InstanceCatalogSignupFragment.InstanceViewHolder holder, int position){
holder.bind(filteredData.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return filteredData.size();
}
@Override
public int getItemViewType(int position){
return -1;
}
@Override
public int getImageCountForItem(int position){
return filteredData.get(position).thumbnailRequest!=null ? 1 : 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return filteredData.get(position).thumbnailRequest;
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.DisableableClickable, ImageLoaderViewHolder{
private final TextView title, description;
private final RadioButton radioButton;
private final ImageView thumbnail;
private boolean enabled;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_catalog, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
radioButton=findViewById(R.id.radiobtn);
thumbnail=findViewById(R.id.image);
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
radioButton.setChecked(chosenInstance==item);
if(item.thumbnailRequest==null)
thumbnail.setImageDrawable(null);
Instance realInstance=instancesCache.get(item.normalizedDomain);
float alpha;
if(realInstance!=null && !realInstance.registrations){
alpha=0.38f;
description.setText(R.string.not_accepting_new_members);
enabled=false;
}else{
alpha=1f;
description.setText(item.description);
enabled=true;
}
title.setAlpha(alpha);
description.setAlpha(alpha);
radioButton.setAlpha(alpha);
thumbnail.setAlpha(alpha);
}
@Override
public void onClick(){
if(chosenInstance==item)
return;
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
boolean found=false;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
found=true;
break;
}
}
if(!found)
adapter.notifyItemChanged(idx);
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
}
@Override
public void setImage(int index, Drawable image){
thumbnail.setImageDrawable(image);
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public boolean isEnabled(){
return enabled;
}
}
private enum SignupSpeedFilter{
ANY,
INSTANT,
REVIEWED
}
private enum CategoryChoice{
GENERAL,
SPECIAL;
public boolean matches(String category){
boolean isGeneral=(category==null || "general".equals(category));
return (this==GENERAL)==isGeneral;
}
}
}

View File

@@ -1,263 +0,0 @@
package org.joinmastodon.android.fragments.onboarding;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.ImageButton;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
private View headerView;
private boolean loadedAutocomplete;
private ImageButton clearBtn;
public InstanceChooserLoginFragment(){
super(R.layout.fragment_login, 10);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
dataLoaded();
setTitle(R.string.login_title);
if(!loadedAutocomplete){
loadAutocompleteServers();
}
}
@Override
protected void proceedWithAuthOrSignup(Instance instance){
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
}
@Override
protected void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
if(currentSearchQuery.length()>0){
boolean foundExactMatch=false;
for(CatalogInstance inst:data){
if(inst.normalizedDomain.contains(currentSearchQuery)){
filteredData.add(inst);
if(inst.normalizedDomain.equals(currentSearchQuery))
foundExactMatch=true;
}
}
if(!foundExactMatch)
filteredData.add(0, fakeInstance);
}
UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals);
for(int i=0;i<list.getChildCount();i++){
list.getChildAt(i).invalidateOutline();
}
}
@Override
protected void doLoadData(int offset, int count){
}
private void loadAutocompleteServers(){
loadedAutocomplete=true;
new GetCatalogInstances(null, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogInstance> result){
data.clear();
data.addAll(sortInstances(result));
}
@Override
public void onError(ErrorResponse error){
}
})
.execNoAuth("");
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
Toolbar toolbar=getToolbar();
toolbar.setElevation(0);
toolbar.setBackground(null);
}
@Override
protected RecyclerView.Adapter getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_login, list, false);
clearBtn=headerView.findViewById(R.id.search_clear);
searchEdit=headerView.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
if(s.length()>0){
fakeInstance.domain=fakeInstance.normalizedDomain=s.toString();
fakeInstance.description=getString(R.string.loading_instance);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof InstanceViewHolder ivh){
ivh.rebind();
}
}
if(filteredData.isEmpty()){
filteredData.add(fakeInstance);
adapter.notifyItemInserted(0);
}
clearBtn.setVisibility(View.VISIBLE);
}else{
clearBtn.setVisibility(View.GONE);
}
}
@Override
public void afterTextChanged(Editable s){
}
});
clearBtn.setOnClickListener(v->searchEdit.setText(""));
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.getChildViewHolder(view) instanceof InstanceViewHolder){
outRect.left=outRect.right=V.dp(16);
}
}
});
((UsableRecyclerView)list).setDrawSelectorOnTop(true);
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
public InstancesAdapter(){
super(imgLoader);
}
@NonNull
@Override
public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new InstanceViewHolder();
}
@Override
public void onBindViewHolder(InstanceViewHolder holder, int position){
holder.bind(filteredData.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getItemCount(){
return filteredData.size();
}
@Override
public int getItemViewType(int position){
return -1;
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description;
private final RadioButton radioButton;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_login, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
radioButton=findViewById(R.id.radiobtn);
radioButton.setMinWidth(0);
radioButton.setMinHeight(0);
itemView.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, getAbsoluteAdapterPosition()==1 ? 0 : V.dp(-4), view.getWidth(), view.getHeight()+(getAbsoluteAdapterPosition()==filteredData.size() ? 0 : V.dp(4)), V.dp(4));
}
});
itemView.setClipToOutline(true);
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
radioButton.setChecked(chosenInstance==item);
}
@Override
public void onClick(){
if(chosenInstance==item)
return;
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
boolean found=false;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
found=true;
break;
}
}
if(!found)
adapter.notifyItemChanged(idx);
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
}
}
}

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
@@ -23,14 +22,14 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends ToolbarFragment{
public class InstanceRulesFragment extends AppKitFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@@ -48,28 +47,31 @@ public class InstanceRulesFragment extends ToolbarFragment{
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
setTitle(R.string.instance_rules_title);
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text);
text.setText(getString(R.string.instance_rules_subtitle, instance.uri));
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
title.setText(R.string.instance_rules_title);
subtitle.setText(getString(R.string.instance_rules_subtitle, instance.uri));
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
return view;
}
@@ -77,15 +79,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
// setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
// view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
protected void onButtonClick(){
@@ -125,22 +119,23 @@ public class InstanceRulesFragment extends ToolbarFragment{
}
private class ItemViewHolder extends BindableViewHolder<Instance.Rule>{
private final TextView text, number;
private final TextView title, subtitle;
private final ImageView checkbox;
public ItemViewHolder(){
super(getActivity(), R.layout.item_server_rule, list);
text=findViewById(R.id.text);
number=findViewById(R.id.number);
super(getActivity(), R.layout.item_report_choice, list);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
checkbox=findViewById(R.id.checkbox);
subtitle.setVisibility(View.GONE);
}
@SuppressLint("DefaultLocale")
@Override
public void onBind(Instance.Rule item){
if(item.parsedText==null){
item.parsedText=HtmlParser.parseLinks(item.text);
}
text.setText(item.parsedText);
number.setText(String.format("%d", getAbsoluteAdapterPosition()));
title.setText(item.parsedText);
}
}
}

View File

@@ -25,15 +25,14 @@ import org.joinmastodon.android.api.MastodonDetailedErrorResponse;
import org.joinmastodon.android.api.requests.accounts.RegisterAccount;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.api.requests.oauth.GetOauthToken;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
import java.io.File;
@@ -50,28 +49,30 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class SignupFragment extends ToolbarFragment{
public class SignupFragment extends AppKitFragment{
private static final int AVATAR_RESULT=198;
private static final String TAG="SignupFragment";
private Instance instance;
private EditText displayName, username, email, password, passwordConfirm, reason;
private FloatingHintEditTextLayout displayNameWrap, usernameWrap, emailWrap, passwordWrap, passwordConfirmWrap, reasonWrap;
private EditText displayName, username, email, password, reason;
private TextView reasonExplain;
private Button btn;
private View buttonBar;
private TextWatcher buttonStateUpdater=new SimpleTextWatcher(e->updateButtonState());
private ImageView avatar;
private APIRequest currentBackgroundRequest;
private Application apiApplication;
private Token apiToken;
private boolean submitAfterGettingToken;
private ProgressDialog progressDialog;
private Uri avatarUri;
private File avatarFile;
private HashSet<EditText> errorFields=new HashSet<>();
@Override
@@ -80,30 +81,25 @@ public class SignupFragment extends ToolbarFragment{
setRetainInstance(true);
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken();
setTitle(R.string.signup_title);
}
@Nullable
@Override
public View onCreateContentView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false);
TextView title=view.findViewById(R.id.title);
TextView domain=view.findViewById(R.id.domain);
displayName=view.findViewById(R.id.display_name);
username=view.findViewById(R.id.username);
email=view.findViewById(R.id.email);
password=view.findViewById(R.id.password);
passwordConfirm=view.findViewById(R.id.password_confirm);
avatar=view.findViewById(R.id.avatar);
reason=view.findViewById(R.id.reason);
reasonExplain=view.findViewById(R.id.reason_explain);
View avaWrap=view.findViewById(R.id.ava_wrap);
displayNameWrap=view.findViewById(R.id.display_name_wrap);
usernameWrap=view.findViewById(R.id.username_wrap);
emailWrap=view.findViewById(R.id.email_wrap);
passwordWrap=view.findViewById(R.id.password_wrap);
passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap);
reasonWrap=view.findViewById(R.id.reason_wrap);
title.setText(getString(R.string.signup_title, instance.uri));
domain.setText('@'+instance.uri);
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@@ -118,20 +114,23 @@ public class SignupFragment extends ToolbarFragment{
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
updateButtonState();
username.addTextChangedListener(buttonStateUpdater);
email.addTextChangedListener(buttonStateUpdater);
password.addTextChangedListener(buttonStateUpdater);
passwordConfirm.addTextChangedListener(buttonStateUpdater);
reason.addTextChangedListener(buttonStateUpdater);
username.addTextChangedListener(new ErrorClearingListener(username));
email.addTextChangedListener(new ErrorClearingListener(email));
password.addTextChangedListener(new ErrorClearingListener(password));
passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm));
reason.addTextChangedListener(new ErrorClearingListener(reason));
avaWrap.setOutlineProvider(OutlineProviders.roundedRect(22));
avaWrap.setClipToOutline(true);
avaWrap.setOnClickListener(v->onAvatarClick());
if(!instance.approvalRequired){
reason.setVisibility(View.GONE);
reasonExplain.setVisibility(View.GONE);
@@ -143,23 +142,10 @@ public class SignupFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
private void onButtonClick(){
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
passwordConfirmWrap.setErrorState();
return;
}
showProgressDialog();
if(currentBackgroundRequest!=null){
submitAfterGettingToken=true;
@@ -174,8 +160,32 @@ public class SignupFragment extends ToolbarFragment{
}
}
private void copyAvatar(Runnable onDone){
// Need to copy the avatar from the content provider to somewhere accessible in case the app gets killed between signup and account activation
Activity activity=getActivity();
MastodonAPIController.runInBackground(()->{
String origName=UiUtils.getFileName(avatarUri);
avatarFile=new File(activity.getCacheDir(), System.currentTimeMillis()+origName.substring(origName.lastIndexOf('.')));
try(InputStream in=activity.getContentResolver().openInputStream(avatarUri);
FileOutputStream out=new FileOutputStream(avatarFile)){
byte[] buf=new byte[10240];
int read;
while((read=in.read(buf))>0){
out.write(buf, 0, read);
}
}catch(IOException x){
Log.w(TAG, "copyAvatar: error copying", x);
}
activity.runOnUiThread(onDone);
});
}
private void submit(){
actuallySubmit();
if(avatarUri!=null && (avatarFile==null || !avatarFile.exists())){
copyAvatar(this::actuallySubmit);
}else{
actuallySubmit();
}
}
private void actuallySubmit(){
@@ -194,7 +204,9 @@ public class SignupFragment extends ToolbarFragment{
fakeAccount.acct=fakeAccount.username=username;
fakeAccount.id="tmp"+System.currentTimeMillis();
fakeAccount.displayName=displayName.getText().toString();
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, new AccountActivationInfo(email, System.currentTimeMillis()));
if(avatarFile!=null)
fakeAccount.avatar=avatarFile.getAbsolutePath();
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, false);
Bundle args=new Bundle();
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
@@ -213,7 +225,6 @@ public class SignupFragment extends ToolbarFragment{
continue;
}
field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n")));
getFieldWrapByName(fieldName).setErrorState();
errorFields.add(field);
if(first){
first=false;
@@ -241,16 +252,6 @@ public class SignupFragment extends ToolbarFragment{
};
}
private FloatingHintEditTextLayout getFieldWrapByName(String name){
return switch(name){
case "email" -> emailWrap;
case "username" -> usernameWrap;
case "password" -> passwordWrap;
case "reason" -> reasonWrap;
default -> null;
};
}
private void showProgressDialog(){
if(progressDialog==null){
progressDialog=new ProgressDialog(getActivity());
@@ -261,7 +262,7 @@ public class SignupFragment extends ToolbarFragment{
}
private void updateButtonState(){
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0));
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && (!instance.approvalRequired || reason.length()>0));
}
private void createAppAndGetToken(){
@@ -323,6 +324,20 @@ public class SignupFragment extends ToolbarFragment{
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==AVATAR_RESULT && resultCode==Activity.RESULT_OK){
avatarUri=data.getData();
if(avatarFile!=null && avatarFile.exists())
avatarFile.delete();
ViewImageLoader.load(avatar, getResources().getDrawable(R.drawable.default_avatar), new UrlImageLoaderRequest(avatarUri, V.dp(100), V.dp(100)));
}
}
private void onAvatarClick(){
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*").addCategory(Intent.CATEGORY_OPENABLE), AVATAR_RESULT);
}
private class ErrorClearingListener implements TextWatcher{
public final EditText editText;

View File

@@ -237,7 +237,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem isdi){
isdi.horizontalInset=V.dp(40+32);

View File

@@ -9,7 +9,6 @@ import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Switch;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
@@ -29,17 +28,15 @@ import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class ReportCommentFragment extends MastodonToolbarFragment{
private String accountID;
private Account reportAccount;
private Button btn;
private View buttonBar, forwardReportItem;
private TextView forwardReportText;
private Switch forwardReportSwitch;
private View buttonBar;
private EditText commentEdit;
private boolean forwardReport;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -80,17 +77,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
commentEdit=view.findViewById(R.id.text);
forwardReportSwitch = view.findViewById(R.id.forward_report_switch);
forwardReportItem = view.findViewById(R.id.forward_report);
forwardReportText = view.findViewById(R.id.forward_report_text);
String domain = reportAccount.getDomain();
if (domain == null) {
forwardReportItem.setVisibility(View.GONE);
} else {
forwardReportItem.setOnClickListener(this::onForwardReportClick);
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
forwardReportSwitch.setChecked(forwardReport = true);
}
return view;
}
@@ -115,7 +102,7 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
ReportReason reason=ReportReason.valueOf(getArguments().getString("reason"));
ArrayList<String> statusIDs=getArguments().getStringArrayList("statusIDs");
ArrayList<String> ruleIDs=getArguments().getStringArrayList("ruleIDs");
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), forwardReport)
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
@@ -136,11 +123,6 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
.exec(accountID);
}
private void onForwardReportClick(View v) {
forwardReport = !forwardReport;
forwardReportSwitch.setChecked(forwardReport);
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))

View File

@@ -164,10 +164,6 @@ public class Account extends BaseModel{
return '@'+acct;
}
public String getShortUsername() {
return '@'+acct.split("@")[0];
}
@Override
public String toString(){
return "Account{"+

View File

@@ -6,14 +6,12 @@ import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;
@Parcel
public class Filter extends BaseModel{
@RequiredField
public String id;
@@ -23,7 +21,6 @@ public class Filter extends BaseModel{
public Instant expiresAt;
public boolean irreversible;
public boolean wholeWord;
public FilterAction filterAction;
@SerializedName("context")
private List<FilterContext> _context;
@@ -79,11 +76,4 @@ public class Filter extends BaseModel{
@SerializedName("thread")
THREAD
}
public enum FilterAction{
@SerializedName("hide")
HIDE,
@SerializedName("warn")
WARN
}
}

View File

@@ -1,8 +0,0 @@
package org.joinmastodon.android.model;
import org.parceler.Parcel;
@Parcel
public class FilterResult extends BaseModel {
public Filter filter;
}

View File

@@ -82,8 +82,6 @@ public class Instance extends BaseModel{
// non-standard field in some Mastodon forks
public int maxTootChars;
public V2 v2;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
@@ -178,19 +176,4 @@ public class Instance extends BaseModel{
public int minExpiration;
public int maxExpiration;
}
@Parcel
public static class V2 extends BaseModel {
public V2.Configuration configuration;
@Parcel
public static class Configuration {
public TranslationConfiguration translation;
}
@Parcel
public static class TranslationConfiguration{
public boolean enabled;
}
}
}

View File

@@ -57,11 +57,6 @@ public class Poll extends BaseModel{
public String title;
public Integer votesCount;
public Option() {}
public Option(String title) {
this.title = title;
}
@Override
public String toString(){
return "Option{"+

View File

@@ -43,9 +43,7 @@ public class PushNotification extends BaseModel{
@SerializedName("follow")
FOLLOW(R.string.notification_type_follow),
@SerializedName("poll")
POLL(R.string.notification_type_poll),
@SerializedName("status")
STATUS(R.string.sk_notification_type_status);
POLL(R.string.notification_type_poll);
@StringRes
public final int localizedName;

View File

@@ -43,11 +43,10 @@ public class PushSubscription extends BaseModel implements Cloneable{
public boolean reblog;
public boolean mention;
public boolean poll;
public boolean status;
public static Alerts ofAll(){
Alerts alerts=new Alerts();
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=alerts.status=true;
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=true;
return alerts;
}
@@ -59,7 +58,6 @@ public class PushSubscription extends BaseModel implements Cloneable{
", reblog="+reblog+
", mention="+mention+
", poll="+poll+
", status="+status+
'}';
}

View File

@@ -1,79 +0,0 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
@Parcel
public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
@RequiredField
public String id;
@RequiredField
public Instant scheduledAt;
@RequiredField
public Params params;
@RequiredField
public List<Attachment> mediaAttachments;
@Override
public String getID() {
return id;
}
@Parcel
public static class Params {
@RequiredField
public String text;
public String spoilerText;
@RequiredField
public StatusPrivacy visibility;
public long inReplyToId;
public ScheduledPoll poll;
public boolean sensitive;
public boolean withRateLimit;
public String language;
public String idempotency;
public String applicationId;
public List<String> mediaIds;
}
@Parcel
public static class ScheduledPoll {
@RequiredField
public String expiresIn;
@RequiredField
public List<String> options;
public boolean multiple;
public boolean hideTotals;
public Poll toPoll() {
Poll p = new Poll();
p.voted = true;
p.emojis = List.of();
p.ownVotes = List.of();
p.multiple = multiple;
p.options = options.stream().map(Option::new).collect(Collectors.toList());
return p;
}
}
public Status toStatus() {
Status s = new Status();
s.id = id;
s.mediaAttachments = mediaAttachments;
s.createdAt = scheduledAt;
s.content = s.text = params.text;
s.spoilerText = params.spoilerText;
s.visibility = params.visibility;
s.language = params.language;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
if (params.poll != null) s.poll = params.poll.toPoll();
return s;
}
}

View File

@@ -40,7 +40,6 @@ public class Status extends BaseModel implements DisplayItemsParent{
public long favouritesCount;
public long repliesCount;
public Instant editedAt;
public List<FilterResult> filtered;
public String url;
public String inReplyToId;

View File

@@ -1,7 +0,0 @@
package org.joinmastodon.android.model;
public class TranslatedStatus extends BaseModel {
public String content;
public String detectedSourceLanguage;
public String provider;
}

View File

@@ -1,10 +1,5 @@
package org.joinmastodon.android.model.catalog;
import android.graphics.Region;
import android.text.TextUtils;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.model.BaseModel;
@@ -12,17 +7,13 @@ import org.joinmastodon.android.model.BaseModel;
import java.net.IDN;
import java.util.List;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
@AllFieldsAreRequired
public class CatalogInstance extends BaseModel{
public String domain;
public String version;
public String description;
public List<String> languages;
@SerializedName("region")
private String _region;
public String region;
public List<String> categories;
public String proxiedThumbnail;
public int totalUsers;
@@ -31,9 +22,7 @@ public class CatalogInstance extends BaseModel{
public String language;
public String category;
public transient Region region;
public transient String normalizedDomain;
public transient UrlImageLoaderRequest thumbnailRequest;
@Override
public void postprocess() throws ObjectValidationException{
@@ -42,14 +31,6 @@ public class CatalogInstance extends BaseModel{
normalizedDomain=IDN.toUnicode(domain);
else
normalizedDomain=domain;
if(!TextUtils.isEmpty(_region)){
try{
region=Region.valueOf(_region.toUpperCase());
}catch(IllegalArgumentException ignore){}
}
if(!TextUtils.isEmpty(proxiedThumbnail)){
thumbnailRequest=new UrlImageLoaderRequest(proxiedThumbnail, 0, V.dp(56));
}
}
@Override
@@ -69,13 +50,4 @@ public class CatalogInstance extends BaseModel{
", category='"+category+'\''+
'}';
}
public enum Region{
EUROPE,
NORTH_AMERICA,
SOUTH_AMERICA,
AFRICA,
ASIA,
OCEANIA
}
}

View File

@@ -2,12 +2,14 @@ package org.joinmastodon.android.ui;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -23,7 +25,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -77,7 +80,7 @@ public class AccountSwitcherSheet extends BottomSheet{
holder.avatar.setImageResource(R.drawable.ic_fluent_add_circle_24_filled);
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary)));
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, ()->{
Nav.go(activity, CustomWelcomeFragment.class, null);
Nav.go(activity, SplashFragment.class, null);
dismiss();
}));
@@ -240,10 +243,7 @@ public class AccountSwitcherSheet extends BottomSheet{
public WrappedAccount(AccountSession session){
this.session=session;
if(session.self.avatar!=null)
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
else
req=null;
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
}
}
}

View File

@@ -28,7 +28,6 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -155,16 +154,11 @@ public class ComposeAutocompleteViewController{
}else if(mode==Mode.EMOJIS){
String _text=text.substring(1); // remove ':'
List<WrappedEmoji> oldList=emojis;
List<Emoji> allEmojis = AccountSessionManager.getInstance()
emojis=AccountSessionManager.getInstance()
.getCustomEmojis(AccountSessionManager.getInstance().getAccount(accountID).domain)
.stream()
.flatMap(ec->ec.emojis.stream())
.filter(e->e.visibleInPicker)
.collect(Collectors.toList());
List<Emoji> startsWithSearch = allEmojis.stream().filter(e -> e.shortcode.toLowerCase().startsWith(_text.toLowerCase())).collect(Collectors.toList());
emojis=Stream.concat(startsWithSearch.stream(), allEmojis.stream()
.filter(e -> !startsWithSearch.contains(e))
.filter(e -> e.shortcode.toLowerCase().contains(_text.toLowerCase())))
.filter(e->e.visibleInPicker && e.shortcode.startsWith(_text))
.map(WrappedEmoji::new)
.collect(Collectors.toList());
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));

View File

@@ -45,7 +45,7 @@ public class ImageDescriptionSheet extends BottomSheet{
}
TextView heading=new TextView(activity);
heading.setText(R.string.sk_image_description);
heading.setText(R.string.image_description);
heading.setAllCaps(true);
heading.setTypeface(null, Typeface.BOLD);
heading.setPadding(0, V.dp(24), 0, V.dp(8));

View File

@@ -4,7 +4,6 @@ import android.app.AlertDialog;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import me.grishka.appkit.utils.V;
@@ -32,16 +31,8 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
if(titleID!=0){
View title=alert.findViewById(titleID);
if(title!=null){
int iconID=getContext().getResources().getIdentifier("icon", "id", "android");
int alertTitleID=getContext().getResources().getIdentifier("alertTitle", "id", "android");
if (alertTitleID != 0 && iconID != 0) {
ImageView icon = title.findViewById(iconID);
if (icon.getDrawable() != null) {
title.findViewById(alertTitleID).setPadding(V.dp(8), 0, 0, 0);
}
}
int pad=V.dp(24);
title.setPadding(pad, pad, pad, V.dp(12));
title.setPadding(pad, pad, pad, pad);
}
}
int titleDividerID=getContext().getResources().getIdentifier("titleDividerNoCustom", "id", "android");

View File

@@ -12,8 +12,6 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Account;
@@ -25,9 +23,7 @@ import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -158,18 +154,10 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
private void onFollowRequestButtonClick(View v) {
itemView.setHasTransientState(true);
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), null, v == acceptButton, relationship, rel -> {
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), item.notification.id , v == acceptButton, relationship, rel -> {
itemView.setHasTransientState(false);
item.parentFragment.putRelationship(item.account.id, rel);
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
if (!rel.requested && !rel.followedBy && adapter != null) {
int index = item.parentFragment.getDisplayItems().indexOf(item);
item.parentFragment.getDisplayItems().remove(index);
item.parentFragment.getDisplayItems().remove(index - 1);
adapter.notifyItemRangeRemoved(getLayoutPosition()-1, 2);
} else {
rebind();
}
rebind();
});
}

View File

@@ -98,7 +98,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
case DIRECT -> R.drawable.ic_at_symbol;
});
}

View File

@@ -1,41 +1,30 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.function.Consumer;
import java.text.DecimalFormat;
import me.grishka.appkit.Nav;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
public class FooterStatusDisplayItem extends StatusDisplayItem{
@@ -57,17 +46,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView reply, boost, favorite, bookmark;
private final ImageView share;
private static final Animation opacityOut, opacityIn;
private View touchingView = null;
private boolean longClickPerformed = false;
private final Runnable longClickRunnable = () -> {
longClickPerformed = touchingView != null && touchingView.performLongClick();
if (longClickPerformed && touchingView != null) {
touchingView.startAnimation(opacityIn);
touchingView.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
}
};
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@Override
@@ -78,16 +56,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
};
static {
opacityOut = new AlphaAnimation(1, 0.55f);
opacityOut.setDuration(300);
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
opacityOut.setFillAfter(true);
opacityIn = new AlphaAnimation(0.55f, 1);
opacityIn.setDuration(400);
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
}
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_footer, parent);
reply=findViewById(R.id.reply);
@@ -106,25 +74,15 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
View favorite=findViewById(R.id.favorite_btn);
View share=findViewById(R.id.share_btn);
View bookmark=findViewById(R.id.bookmark_btn);
reply.setOnTouchListener(this::onButtonTouch);
reply.setOnClickListener(this::onReplyClick);
reply.setOnLongClickListener(this::onReplyLongClick);
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
boost.setOnTouchListener(this::onButtonTouch);
boost.setOnClickListener(this::onBoostClick);
boost.setOnLongClickListener(this::onBoostLongClick);
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
favorite.setOnTouchListener(this::onButtonTouch);
favorite.setOnClickListener(this::onFavoriteClick);
favorite.setOnLongClickListener(this::onFavoriteLongClick);
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
bookmark.setOnTouchListener(this::onButtonTouch);
bookmark.setOnClickListener(this::onBookmarkClick);
bookmark.setOnLongClickListener(this::onBookmarkLongClick);
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
share.setOnTouchListener(this::onButtonTouch);
share.setOnClickListener(this::onShareClick);
share.setOnLongClickListener(this::onShareLongClick);
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
}
@@ -133,7 +91,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount);
bindButton(favorite, item.status.favouritesCount);
reply.setSelected(item.status.repliesCount > 0);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
@@ -151,204 +108,37 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
}
private boolean onButtonTouch(View v, MotionEvent event){
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
touchingView = null;
v.removeCallbacks(longClickRunnable);
if (!longClickPerformed) v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
if (disabled) return true;
if (action == MotionEvent.ACTION_UP && !longClickPerformed) v.performClick();
else if (!longClickPerformed) v.startAnimation(opacityIn);
} else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false;
touchingView = v;
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
v.setPivotX(V.dp(20));
v.animate().scaleX(0.85f).scaleY(0.85f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(75).start();
if (disabled) return true;
v.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
v.startAnimation(opacityOut);
}
return true;
}
private void onReplyClick(View v){
v.startAnimation(opacityIn);
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putParcelable("replyTo", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}
private boolean onReplyLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
Bundle args=new Bundle();
String accountID = session.getID();
args.putString("account", accountID);
UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> {
args.putParcelable("replyTo", Parcels.wrap(status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
}, null);
return true;
}
private void onBoostClick(View v){
boost.setSelected(!item.status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
}
private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn);
bindButton(boost, r.reblogsCount);
}
private boolean onBoostLongClick(View v){
Context ctx = itemView.getContext();
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
AccountSession session = AccountSessionManager.getInstance().getAccount(item.accountID);
Consumer<StatusPrivacy> doReblog = (visibility) -> {
v.startAnimation(opacityOut);
session.getStatusInteractionController()
.setReblogged(item.status, !item.status.reblogged, visibility, r->boostConsumer(v, r));
dialog.dismiss();
};
View separator = menu.findViewById(R.id.separator);
TextView reblogHeader = menu.findViewById(R.id.reblog_header);
TextView undoReblog = menu.findViewById(R.id.delete_reblog);
TextView reblogAs = menu.findViewById(R.id.reblog_as);
TextView itemPublic = menu.findViewById(R.id.vis_public);
TextView itemUnlisted = menu.findViewById(R.id.vis_unlisted);
TextView itemFollowers = menu.findViewById(R.id.vis_followers);
undoReblog.setVisibility(item.status.reblogged ? View.VISIBLE : View.GONE);
separator.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
reblogHeader.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
reblogAs.setVisibility(AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1 ? View.VISIBLE : View.GONE);
itemPublic.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PUBLIC) ? View.GONE : View.VISIBLE);
itemUnlisted.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) ? View.GONE : View.VISIBLE);
itemFollowers.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PRIVATE) ? View.GONE : View.VISIBLE);
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_community_24_regular);
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_checkmark_24_regular);
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
// e.g. post visibility is unlisted, but default is public
// in this case, we want to display the check mark on the most visible visibility
if (defaultVisibility != null && item.status.visibility.isLessVisibleThan(defaultVisibility)) {
for (StatusPrivacy vis : StatusPrivacy.values()) {
if (vis.equals(item.status.visibility)) {
defaultVisibility = vis;
break;
}
}
}
itemPublic.setCompoundDrawablesWithIntrinsicBounds(publicDrawable, null, StatusPrivacy.PUBLIC.equals(defaultVisibility) ? checkMark : null, null);
itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null);
itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null);
undoReblog.setOnClickListener(c->doReblog.accept(null));
itemPublic.setOnClickListener(c->doReblog.accept(StatusPrivacy.PUBLIC));
itemUnlisted.setOnClickListener(c->doReblog.accept(StatusPrivacy.UNLISTED));
itemFollowers.setOnClickListener(c->doReblog.accept(StatusPrivacy.PRIVATE));
reblogAs.setOnClickListener(c->{
dialog.dismiss();
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.reblogged,
(ic, status, consumer) -> ic.setReblogged(status, true, null, consumer),
R.string.sk_reblog_as,
R.string.sk_reblogged_as,
R.string.sk_already_reblogged,
// TODO: replace once available: https://raw.githubusercontent.com/microsoft/fluentui-system-icons/main/android/library/src/main/res/drawable/ic_fluent_arrow_repeat_all_28_regular.xml
R.drawable.ic_fluent_arrow_repeat_all_24_regular
);
});
menu.findViewById(R.id.quote).setOnClickListener(c->{
dialog.dismiss();
v.startAnimation(opacityIn);
Bundle args=new Bundle();
args.putString("account", item.accountID);
StringBuilder prefilledText = new StringBuilder().append("\n\n");
String ownID = AccountSessionManager.getInstance().getAccount(item.accountID).self.id;
if (!item.status.account.id.equals(ownID)) prefilledText.append('@').append(item.status.account.acct).append(' ');
prefilledText.append(item.status.url);
args.putString("prefilledText", prefilledText.toString());
args.putInt("selectionStart", 0);
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
dialog.show();
return true;
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged);
boost.setSelected(item.status.reblogged);
bindButton(boost, item.status.reblogsCount);
}
private void onFavoriteClick(View v){
favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
v.startAnimation(opacityIn);
bindButton(favorite, r.favouritesCount);
});
}
private boolean onFavoriteLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.favourited,
(ic, status, consumer) -> ic.setFavorited(status, true, consumer),
R.string.sk_favorite_as,
R.string.sk_favorited_as,
R.string.sk_already_favorited,
R.drawable.ic_fluent_star_28_regular
);
return true;
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited);
favorite.setSelected(item.status.favourited);
bindButton(favorite, item.status.favouritesCount);
}
private void onBookmarkClick(View v){
bookmark.setSelected(!item.status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
v.startAnimation(opacityIn);
});
}
private boolean onBookmarkLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.bookmarked,
(ic, status, consumer) -> ic.setBookmarked(status, true, consumer),
R.string.sk_bookmark_as,
R.string.sk_bookmarked_as,
R.string.sk_already_bookmarked,
R.drawable.ic_fluent_bookmark_28_regular
);
return true;
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
bookmark.setSelected(item.status.bookmarked);
}
private void onShareClick(View v){
v.startAnimation(opacityIn);
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, item.status.url);
v.getContext().startActivity(Intent.createChooser(intent, v.getContext().getString(R.string.share_toot_title)));
}
private boolean onShareLongClick(View v){
UiUtils.copyText(v, item.status.url);
return true;
}
private int descriptionForId(int id){
if(id==R.id.reply_btn)
return R.string.button_reply;
@@ -357,7 +147,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
if(id==R.id.favorite_btn)
return R.string.button_favorite;
if(id==R.id.bookmark_btn)
return R.string.add_bookmark;
return R.string.button_bookmark;
if(id==R.id.share_btn)
return R.string.button_share;
return 0;

View File

@@ -11,7 +11,6 @@ import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
@@ -23,21 +22,16 @@ import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
@@ -45,12 +39,8 @@ import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
@@ -72,20 +62,15 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private boolean hasVisibilityToggle;
boolean needBottomPadding;
private String extraText;
private Notification notification;
private ScheduledStatus scheduledStatus;
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText, Notification notification, ScheduledStatus scheduledStatus){
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
super(parentID, parentFragment);
user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user;
this.user=user;
this.createdAt=createdAt;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName);
this.status=status;
this.notification=notification;
this.scheduledStatus=scheduledStatus;
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
emojiHelper.setText(parsedName);
if(status!=null){
@@ -122,7 +107,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView name, username, timestamp, extraText;
private final ImageView avatar, more, visibility, deleteNotification;
private final ImageView avatar, more, visibility;
private final PopupMenu optionsMenu;
private Relationship relationship;
private APIRequest<?> currentRelationshipRequest;
@@ -142,25 +127,18 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
visibility=findViewById(R.id.visibility);
deleteNotification=findViewById(R.id.delete_notification);
extraText=findViewById(R.id.extra_text);
avatar.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(roundCornersOutline);
avatar.setClipToOutline(true);
more.setOnClickListener(this::onMoreClick);
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
deleteNotification.setOnClickListener(v->UiUtils.confirmDeleteNotification(activity, item.parentFragment.getAccountID(), item.notification, ()->{
if (item.parentFragment instanceof NotificationsListFragment fragment) {
fragment.removeNotification(item.notification);
}
}));
optionsMenu=new PopupMenu(activity, more);
optionsMenu.inflate(R.menu.post);
optionsMenu.setOnMenuItemClickListener(menuItem->{
Account account=item.user;
int id=menuItem.getItemId();
if(id==R.id.edit || id==R.id.delete_and_redraft) {
final Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
@@ -175,12 +153,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text);
args.putString("sourceSpoiler", item.status.spoilerText);
args.putBoolean("redraftStatus", true);
args.putParcelable("scheduledStatus", Parcels.wrap(item.scheduledStatus));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else{
new GetStatusSourceText(item.status.id)
.setCallback(new Callback<>(){
@@ -206,12 +178,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
.exec(item.parentFragment.getAccountID());
}
}else if(id==R.id.delete){
if (item.scheduledStatus != null) {
UiUtils.confirmDeleteScheduledPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.scheduledStatus, ()->{});
} else {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}
}else if(id==R.id.pin || id==R.id.unpin) {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}else if(id==R.id.pin || id==R.id.unpin){
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
@@ -223,10 +191,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putParcelable("status", Parcels.wrap(item.status));
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser) {
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(activity, item.status.url);
}else if(id==R.id.copy_link){
UiUtils.copyText(parent, item.status.url);
}else if(id==R.id.follow){
if(relationship==null)
return true;
@@ -240,7 +206,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
progress.dismiss();
}, rel->{
relationship=rel;
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getShortUsername()), Toast.LENGTH_SHORT).show();
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getDisplayUsername()), Toast.LENGTH_SHORT).show();
});
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
@@ -249,37 +215,17 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
return true;
});
UiUtils.enablePopupMenuIcons(activity, optionsMenu);
}
private void populateAccountsMenu(Menu menu) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
sessions.stream().filter(s -> !s.getID().equals(item.accountID)).forEach(s -> {
String username = "@"+s.self.username+"@"+s.domain;
menu.add(username).setOnMenuItemClickListener(c->{
UiUtils.openURL(item.parentFragment.getActivity(), s.getID(), item.status.url, false);
return true;
});
});
}
@Override
public void onBind(HeaderStatusDisplayItem item){
name.setText(item.parsedName);
username.setText('@'+item.user.acct);
if (item.scheduledStatus!=null)
if (item.scheduledStatus.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT)) {
timestamp.setText(R.string.sk_draft);
} else {
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter));
}
else if(item.status==null || item.status.editedAt==null)
if(item.status==null || item.status.editedAt==null)
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
else
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
if(item.hasVisibilityToggle){
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
@@ -352,30 +298,15 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void updateOptionsMenu(){
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
Menu menu=optionsMenu.getMenu();
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
SubMenu accountsMenu = openWithAccounts != null ? openWithAccounts.getSubMenu() : null;
if (hasMultipleAccounts && accountsMenu != null) {
openWithAccounts.setVisible(true);
accountsMenu.clear();
populateAccountsMenu(accountsMenu);
} else if (openWithAccounts != null) {
openWithAccounts.setVisible(false);
}
Account account=item.user;
Menu menu=optionsMenu.getMenu();
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
boolean isPostScheduled=item.scheduledStatus!=null;
menu.findItem(R.id.open_with_account).setVisible(!isPostScheduled && hasMultipleAccounts);
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.delete_and_redraft).setVisible(!isPostScheduled && item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(!isPostScheduled && item.status!=null);
menu.findItem(R.id.copy_link).setVisible(!isPostScheduled && item.status!=null);
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
MenuItem blockDomain=menu.findItem(R.id.block_domain);
MenuItem mute=menu.findItem(R.id.mute);
MenuItem block=menu.findItem(R.id.block);
@@ -391,7 +322,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
bookmark.setVisible(false);
}
*/
if(isPostScheduled || isOwnPost){
if(isOwnPost){
mute.setVisible(false);
block.setVisible(false);
report.setVisible(false);
@@ -402,22 +333,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
block.setVisible(true);
report.setVisible(true);
follow.setVisible(relationship==null || relationship.following || (!relationship.blocking && !relationship.blockedBy && !relationship.domainBlocking && !relationship.muting));
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute);
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getShortUsername()));
// disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place
// if(!account.isLocal()){
// blockDomain.setVisible(true);
// blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
// }else{
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getDisplayUsername()));
if(!account.isLocal()){
blockDomain.setVisible(true);
blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
}else{
blockDomain.setVisible(false);
// }
boolean following = relationship!=null && relationship.following;
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, account.getShortUsername()));
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow);
}
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.getDisplayUsername()));
}
}
}

View File

@@ -95,7 +95,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
}
private void onClick(View v){
UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), item.status.card.url);
UiUtils.launchWebBrowser(itemView.getContext(), item.status.card.url);
}
}
}

View File

@@ -6,7 +6,6 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Poll;
@@ -45,7 +44,7 @@ public class PollFooterStatusDisplayItem extends StatusDisplayItem{
text+=" · "+item.parentFragment.getString(R.string.poll_closed);
}
this.text.setText(text);
button.setVisibility(item.poll.isExpired() || item.poll.voted || (!item.poll.multiple && !GlobalUserPreferences.voteButtonForSingleChoice) ? View.GONE : View.VISIBLE);
button.setVisibility(item.poll.isExpired() || item.poll.voted || !item.poll.multiple ? View.GONE : View.VISIBLE);
button.setEnabled(item.poll.selectedOptions!=null && !item.poll.selectedOptions.isEmpty());
}
}

View File

@@ -5,7 +5,6 @@ import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -61,8 +60,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<PollOptionStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView text, percent;
private final View button;
private final ImageView icon;
private final View icon, button;
private final Drawable progressBg;
public Holder(Activity activity, ViewGroup parent){
@@ -78,24 +76,17 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(PollOptionStatusDisplayItem item){
text.setText(item.text);
icon.setVisibility(item.showResults ? View.GONE : View.VISIBLE);
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
itemView.setClickable(!item.showResults);
icon.setImageDrawable(itemView.getContext().getDrawable(item.poll.multiple ?
item.showResults ? R.drawable.ic_poll_checkbox_regular_selector : R.drawable.ic_poll_checkbox_filled_selector :
item.showResults ? R.drawable.ic_poll_option_button : R.drawable.ic_fluent_radio_button_24_selector
));
if(item.showResults){
progressBg.setLevel(Math.round(10000f*item.votesFraction));
button.setBackground(progressBg);
itemView.setSelected(item.isMostVoted);
icon.setSelected(item.poll.ownVotes.contains(item.poll.options.indexOf(item.option)));
icon.setVisibility(item.poll.voted && item.poll.ownVotes.isEmpty() ? View.GONE : View.VISIBLE);
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
}else{
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
icon.setSelected(itemView.isSelected());
icon.setVisibility(View.VISIBLE);
}
}

View File

@@ -1,13 +1,9 @@
package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.MastodonApp.context;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.SpannableStringBuilder;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -15,7 +11,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -32,13 +27,10 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private CharSequence text;
@DrawableRes
private int icon;
private StatusPrivacy visibility;
@DrawableRes
private int iconEnd;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private View.OnClickListener handleClick;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick){
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, @Nullable View.OnClickListener handleClick){
super(parentID, parentFragment);
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
HtmlParser.parseCustomEmoji(ssb, emojis);
@@ -46,19 +38,6 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
emojiHelper.setText(ssb);
this.icon=icon;
this.handleClick=handleClick;
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
updateVisibility(visibility);
}
public void updateVisibility(StatusPrivacy visibility) {
this.visibility = visibility;
this.iconEnd = visibility != null ? switch (visibility) {
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
default -> 0;
} : 0;
}
@Override
@@ -86,18 +65,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
text.setText(item.text);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0);
if(item.handleClick!=null) text.setOnClickListener(item.handleClick);
text.setEnabled(!item.inset);
text.setClickable(!item.inset);
Context ctx = itemView.getContext();
int visibilityText = item.visibility != null ? switch (item.visibility) {
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
default -> 0;
} : 0;
if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")");
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
}

View File

@@ -8,16 +8,13 @@ import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
@@ -76,29 +73,26 @@ public abstract class StatusDisplayItem{
};
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
Status statusForContent=status.getContentStatus();
Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
if(status.reblog!=null){
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, null, i->{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, i->{
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
}
HeaderStatusDisplayItem header;
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus));
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
else

View File

@@ -9,31 +9,19 @@ import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Button;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.TranslatedStatus;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.LinkedTextView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.MovieDrawable;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class TextStatusDisplayItem extends StatusDisplayItem{
@@ -42,9 +30,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
private CharSequence parsedSpoilerText;
public boolean textSelectable;
public final Status status;
public boolean translated = false;
public TranslatedStatus translation = null;
private AccountSession session;
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status){
super(parentID, parentFragment);
@@ -56,7 +41,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerEmojiHelper=new CustomEmojiHelper();
spoilerEmojiHelper.setText(parsedSpoilerText);
}
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
}
@Override
@@ -81,10 +65,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
private final LinkedTextView text;
private final LinearLayout spoilerHeader;
private final TextView spoilerTitle, spoilerTitleInline, translateInfo;
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress;
private final int backgroundColor, borderColor;
private final Button translateButton;
private final TextView spoilerTitle, spoilerTitleInline;
private final View spoilerOverlay, borderTop, borderBottom;
private final Drawable backgroundColor, borderColor;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_text, parent);
@@ -95,90 +78,47 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerOverlay=findViewById(R.id.spoiler_overlay);
borderTop=findViewById(R.id.border_top);
borderBottom=findViewById(R.id.border_bottom);
textWrap=findViewById(R.id.text_wrap);
translateWrap=findViewById(R.id.translate_wrap);
translateButton=findViewById(R.id.translate_btn);
translateInfo=findViewById(R.id.translate_info);
translateProgress=findViewById(R.id.translate_progress);
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
backgroundColor=UiUtils.getThemeColor(activity, R.attr.colorBackgroundLight);
borderColor=UiUtils.getThemeColor(activity, R.attr.colorPollVoted);
TypedValue outValue=new TypedValue();
activity.getTheme().resolveAttribute(R.attr.colorBackgroundLight, outValue, true);
backgroundColor=activity.getDrawable(outValue.resourceId);
// activity.getTheme().resolveAttribute(R.attr.colorBackgroundLightest, outValue, true);
// backgroundColorInset=activity.getDrawable(outValue.resourceId);
activity.getTheme().resolveAttribute(R.attr.colorPollVoted, outValue, true);
borderColor=activity.getDrawable(outValue.resourceId);
}
@Override
public void onBind(TextStatusDisplayItem item){
text.setText(item.translated
? HtmlParser.parse(item.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID())
: item.text);
text.setText(item.text);
text.setTextIsSelectable(item.textSelectable);
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false);
spoilerTitleInline.setBackgroundColor(item.inset ? 0 : backgroundColor);
spoilerTitleInline.setBackground(item.inset ? null : backgroundColor);
spoilerTitleInline.setPadding(spoilerTitleInline.getPaddingLeft(), item.inset ? 0 : V.dp(14), spoilerTitleInline.getPaddingRight(), item.inset ? 0 : V.dp(14));
borderTop.setBackgroundColor(item.inset ? 0 : borderColor);
borderBottom.setBackgroundColor(item.inset ? 0 : borderColor);
borderTop.setBackground(item.inset ? null : borderColor);
borderBottom.setBackground(item.inset ? null : borderColor);
if(!TextUtils.isEmpty(item.status.spoilerText)){
spoilerTitle.setText(item.parsedSpoilerText);
spoilerTitleInline.setText(item.parsedSpoilerText);
if(item.status.spoilerRevealed){
spoilerOverlay.setVisibility(View.GONE);
spoilerHeader.setVisibility(View.VISIBLE);
textWrap.setVisibility(View.VISIBLE);
text.setVisibility(View.VISIBLE);
itemView.setClickable(false);
}else{
spoilerOverlay.setVisibility(View.VISIBLE);
spoilerHeader.setVisibility(View.GONE);
textWrap.setVisibility(View.GONE);
text.setVisibility(View.GONE);
itemView.setClickable(true);
}
}else{
spoilerOverlay.setVisibility(View.GONE);
spoilerHeader.setVisibility(View.GONE);
textWrap.setVisibility(View.VISIBLE);
text.setVisibility(View.VISIBLE);
itemView.setClickable(false);
}
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain);
boolean translateEnabled = instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled;
translateWrap.setVisibility(
(!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable) &&
translateEnabled &&
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
item.status.language != null &&
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))
? View.VISIBLE : View.GONE);
translateButton.setText(item.translated ? R.string.sk_translate_show_original : R.string.sk_translate_post);
translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, item.translation.provider) : "");
translateButton.setOnClickListener(v->{
if (item.translation == null) {
translateProgress.setVisibility(View.VISIBLE);
translateButton.setClickable(false);
translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
new TranslateStatus(item.status.id).setCallback(new Callback<>() {
@Override
public void onSuccess(TranslatedStatus translatedStatus) {
item.translation = translatedStatus;
item.translated = true;
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
rebind();
}
@Override
public void onError(ErrorResponse error) {
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
error.showToast(itemView.getContext());
}
}).exec(item.parentFragment.getAccountID());
} else {
item.translated = !item.translated;
rebind();
}
});
}
@Override

View File

@@ -178,7 +178,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
toolbar=uiOverlay.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
imageDescriptionButton = toolbar.getMenu()
.add(R.string.sk_image_description)
.add(R.string.image_description)
.setIcon(R.drawable.ic_fluent_image_alt_text_24_regular)
.setVisible(attachments.get(pager.getCurrentItem()).description != null
&& !attachments.get(pager.getCurrentItem()).description.isEmpty())

View File

@@ -10,8 +10,6 @@ import android.text.Layout;
import android.text.Spanned;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView;
import me.grishka.appkit.utils.V;
@@ -22,11 +20,7 @@ public class ClickableLinksDelegate {
private Path hlPath;
private LinkSpan selectedSpan;
private TextView view;
private final Runnable longClickRunnable = () -> {
if (selectedSpan != null) selectedSpan.onLongClick(view);
};
public ClickableLinksDelegate(TextView view) {
this.view=view;
hlPaint=new Paint();
@@ -36,7 +30,6 @@ public class ClickableLinksDelegate {
}
public boolean onTouch(MotionEvent event) {
long eventDuration = event.getEventTime() - event.getDownTime();
if(event.getAction()==MotionEvent.ACTION_DOWN){
int line=-1;
Rect rect=new Rect();
@@ -70,7 +63,6 @@ public class ClickableLinksDelegate {
}
hlPath=new Path();
selectedSpan=span;
view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){
@@ -98,11 +90,8 @@ public class ClickableLinksDelegate {
}
}
if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){
if (eventDuration <= ViewConfiguration.getLongPressTimeout()) {
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
}
view.removeCallbacks(longClickRunnable);
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
hlPath=null;
selectedSpan=null;
view.invalidate();
@@ -111,7 +100,6 @@ public class ClickableLinksDelegate {
if(event.getAction()==MotionEvent.ACTION_CANCEL){
hlPath=null;
selectedSpan=null;
view.removeCallbacks(longClickRunnable);
view.invalidate();
return false;
}

View File

@@ -1,29 +1,12 @@
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;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import android.util.TypedValue;
import android.widget.TextView;
import 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;
@@ -32,11 +15,11 @@ import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.safety.Cleaner;
import org.jsoup.safety.Safelist;
import org.jsoup.select.NodeVisitor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -46,8 +29,6 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
public class HtmlParser{
private static final String TAG="HtmlParser";
private static final String VALID_URL_PATTERN_STRING =
@@ -86,17 +67,11 @@ public class HtmlParser{
public Object span;
public int start;
public Element element;
public boolean more;
public SpanInfo(Object span, int start, Element element){
this(span, start, element, false);
}
public SpanInfo(Object span, int start, Element element, boolean more){
this.span=span;
this.start=start;
this.element=element;
this.more=more;
}
}
@@ -111,14 +86,14 @@ public class HtmlParser{
@Override
public void head(@NonNull Node node, int depth){
if(node instanceof TextNode textNode){
ssb.append(textNode.getWholeText());
ssb.append(textNode.text());
}else if(node instanceof Element el){
switch(el.nodeName()){
case "a" -> {
String href=el.attr("href");
LinkSpan.Type linkType;
String text=el.text();
if(el.hasClass("hashtag")){
String text=el.text();
if(text.startsWith("#")){
linkType=LinkSpan.Type.HASHTAG;
href=text.substring(1);
@@ -136,7 +111,7 @@ public class HtmlParser{
}else{
linkType=LinkSpan.Type.URL;
}
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, text), ssb.length(), el));
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el));
}
case "br" -> ssb.append('\n');
case "span" -> {
@@ -144,59 +119,24 @@ public class HtmlParser{
openSpans.add(new SpanInfo(new InvisibleSpan(), ssb.length(), el));
}
}
case "li" -> openSpans.add(new SpanInfo(new BulletSpan(V.dp(8)), ssb.length(), el));
case "em", "i" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.ITALIC), ssb.length(), el));
case "h1", "h2", "h3", "h4", "h5", "h6" -> {
// increase line height above heading (multiplying the margin)
if (node.previousSibling()!=null) ssb.setSpan(new RelativeSizeSpan(2), ssb.length() - 1, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (!node.nodeName().equals("h1")) {
openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
}
openSpans.add(new SpanInfo(new RelativeSizeSpan(switch(node.nodeName()) {
case "h1" -> 1.5f;
case "h2" -> 1.25f;
case "h3" -> 1.125f;
default -> 1;
}), ssb.length(), el, !node.nodeName().equals("h1")));
}
case "strong", "b" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
case "u" -> openSpans.add(new SpanInfo(new UnderlineSpan(), ssb.length(), el));
case "s", "del" -> openSpans.add(new SpanInfo(new StrikethroughSpan(), ssb.length(), el));
case "sub", "sup" -> {
openSpans.add(new SpanInfo(node.nodeName().equals("sub") ? new SubscriptSpan() : new SuperscriptSpan(), ssb.length(), el));
openSpans.add(new SpanInfo(new RelativeSizeSpan(0.8f), ssb.length(), el, true));
}
case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el));
case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el));
}
}
}
final static List<String> blockElements = Arrays.asList("p", "ul", "ol", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6");
@Override
public void tail(@NonNull Node node, int depth){
if(node instanceof Element el){
processOpenSpan(el);
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
ssb.append("", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}else if(blockElements.contains(el.nodeName()) && node.nextSibling()!=null){
ssb.append("\n"); // line end
ssb.append("\n", new RelativeSizeSpan(0.65f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block
}
}
}
private void processOpenSpan(Element el) {
if(!openSpans.isEmpty()){
SpanInfo si=openSpans.get(openSpans.size()-1);
if(si.element==el){
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
openSpans.remove(openSpans.size()-1);
if(si.more) processOpenSpan(el);
}
if("li".equals(el.nodeName()) && el.nextSibling()!=null) {
ssb.append('\n');
}else if("p".equals(el.nodeName())){
if(node.nextSibling()!=null)
ssb.append("\n\n");
}else if(!openSpans.isEmpty()){
SpanInfo si=openSpans.get(openSpans.size()-1);
if(si.element==el){
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
openSpans.remove(openSpans.size()-1);
}
}
}
}
@@ -260,7 +200,7 @@ public class HtmlParser{
String url=matcher.group(3);
if(TextUtils.isEmpty(matcher.group(4)))
url="http://"+url;
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, url), matcher.start(3), matcher.end(3), 0);
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0);
}while(matcher.find()); // Find more URLs
return ssb;
}

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.view.View;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -14,14 +13,12 @@ public class LinkSpan extends CharacterStyle {
private String link;
private Type type;
private String accountID;
private String text;
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){
this.listener=listener;
this.link=link;
this.type=type;
this.accountID=accountID;
this.text=text;
}
public int getColor(){
@@ -41,10 +38,6 @@ public class LinkSpan extends CharacterStyle {
}
}
public void onLongClick(View view) {
UiUtils.copyText(view, getType() == Type.URL ? link : text);
}
public String getLink(){
return link;
}

View File

@@ -1,68 +0,0 @@
package org.joinmastodon.android.ui.utils;
import static org.joinmastodon.android.GlobalUserPreferences.ColorPreference;
import static org.joinmastodon.android.GlobalUserPreferences.ThemePreference;
import static org.joinmastodon.android.GlobalUserPreferences.theme;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.StyleRes;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import java.util.Map;
public class ColorPalette {
public static final Map<GlobalUserPreferences.ColorPreference, ColorPalette> palettes = Map.of(
ColorPreference.MATERIAL3, new ColorPalette(R.style.ColorPalette_Material3)
.dark(R.style.ColorPalette_Material3_Dark, R.style.ColorPalette_Material3_AutoLightDark),
ColorPreference.PINK, new ColorPalette(R.style.ColorPalette_Pink),
ColorPreference.PURPLE, new ColorPalette(R.style.ColorPalette_Purple),
ColorPreference.GREEN, new ColorPalette(R.style.ColorPalette_Green),
ColorPreference.BLUE, new ColorPalette(R.style.ColorPalette_Blue),
ColorPreference.BROWN, new ColorPalette(R.style.ColorPalette_Brown),
ColorPreference.RED, new ColorPalette(R.style.ColorPalette_Red),
ColorPreference.YELLOW, new ColorPalette(R.style.ColorPalette_Yellow)
);
private @StyleRes int base;
private @StyleRes int autoDark;
private @StyleRes int light;
private @StyleRes int dark;
private @StyleRes int black;
private @StyleRes int autoBlack;
public ColorPalette(@StyleRes int baseRes) { base = baseRes; }
public ColorPalette(@StyleRes int lightRes, @StyleRes int darkRes, @StyleRes int autoDarkRes, @StyleRes int blackRes, @StyleRes int autoBlackRes) {
light = lightRes;
dark = darkRes;
autoDark = autoDarkRes;
black = blackRes;
autoBlack = autoBlackRes;
}
public ColorPalette light(@StyleRes int res) { light = res; return this; }
public ColorPalette dark(@StyleRes int res, @StyleRes int auto) { dark = res; autoDark = auto; return this; }
public ColorPalette black(@StyleRes int res, @StyleRes int auto) { dark = res; autoBlack = auto; return this; }
public void apply(Context context) {
if (!((dark != 0 && autoDark != 0) || (black != 0 && autoBlack != 0) || light != 0 || base != 0)) {
throw new IllegalStateException("Invalid color scheme definition");
}
Resources.Theme t = context.getTheme();
if (base != 0) t.applyStyle(base, true);
if (light != 0 && theme.equals(ThemePreference.LIGHT)) t.applyStyle(light, true);
else if (theme.equals(ThemePreference.DARK)) {
if (dark != 0 && !trueBlackTheme) t.applyStyle(dark, true);
else if (black != 0 && trueBlackTheme) t.applyStyle(black, true);
} else if (theme.equals(ThemePreference.AUTO)) {
if (autoDark != 0 && !trueBlackTheme) t.applyStyle(autoDark, true);
else if (autoBlack != 0 && trueBlackTheme) t.applyStyle(autoBlack, true);
}
}
}

View File

@@ -36,7 +36,7 @@ public class DiscoverInfoBannerHelper{
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
case TRENDING_LINKS -> R.string.trending_links_info_banner;
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.federated_timeline_info_banner;
});
}
}

View File

@@ -1,17 +1,9 @@
package org.joinmastodon.android.ui.utils;
import static org.joinmastodon.android.GlobalUserPreferences.theme;
import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -28,10 +20,9 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -45,28 +36,23 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest;
import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
import org.joinmastodon.android.api.requests.notifications.DismissNotification;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
@@ -74,21 +60,16 @@ import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.SpacerSpan;
import org.parceler.Parcels;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -99,12 +80,9 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.browser.customtabs.CustomTabsIntent;
@@ -348,29 +326,22 @@ public class UiUtils{
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed);
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), onConfirmed);
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed){
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed);
}
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, int icon, Runnable onConfirmed){
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, Runnable onConfirmed){
new M3AlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(confirmButton, (dlg, i)->onConfirmed.run())
.setNegativeButton(R.string.cancel, null)
.setIcon(icon)
.show();
}
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
currentlyBlocked ? R.drawable.ic_fluent_person_28_regular : R.drawable.ic_fluent_person_prohibited_28_regular,
()->{
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
new SetAccountBlocked(account.id, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
@@ -394,9 +365,7 @@ public class UiUtils{
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
R.drawable.ic_fluent_shield_28_regular,
()->{
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
new SetDomainBlocked(domain, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
@@ -417,9 +386,7 @@ public class UiUtils{
public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title),
activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName),
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute),
currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular,
()->{
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), ()->{
new SetAccountMuted(account.id, !currentlyMuted)
.setCallback(new Callback<>(){
@Override
@@ -444,61 +411,31 @@ public class UiUtils{
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){
showConfirmationAlert(activity,
forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title,
forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete,
forRedraft ? R.string.sk_delete_and_redraft : R.string.delete,
forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
showConfirmationAlert(activity, forRedraft ? R.string.confirm_delete_and_redraft_title : R.string.confirm_delete_title, forRedraft ? R.string.confirm_delete_and_redraft : R.string.confirm_delete, forRedraft ? R.string.delete_and_redraft : R.string.delete, ()->{
new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID)
);
}
public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback){
boolean isDraft = status.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT);
showConfirmationAlert(activity,
isDraft ? R.string.sk_confirm_delete_draft_title : R.string.sk_confirm_delete_scheduled_post_title,
isDraft ? R.string.sk_confirm_delete_draft : R.string.sk_confirm_delete_scheduled_post,
R.string.delete,
R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus.Scheduled(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object nothing){
resultCallback.run();
E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID)
);
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID);
});
}
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
showConfirmationAlert(activity,
pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title,
pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post,
pinned ? R.string.sk_pin_post : R.string.sk_unpin_post,
pinned ? R.drawable.ic_fluent_pin_off_28_regular : R.drawable.ic_fluent_pin_28_regular,
pinned ? R.string.confirm_pin_post_title : R.string.confirm_unpin_post_title,
pinned ? R.string.confirm_pin_post : R.string.confirm_unpin_post,
pinned ? R.string.pin_post : R.string.unpin_post,
()->{
new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() {
@@ -515,32 +452,12 @@ public class UiUtils{
error.showToast(activity);
}
})
.wrapProgress(activity, pinned ? R.string.sk_pinning : R.string.sk_unpinning, false)
.wrapProgress(activity, pinned ? R.string.pinning : R.string.unpinning, false)
.exec(accountID);
}
);
}
public static void confirmDeleteNotification(Activity activity, String accountID, Notification notification, Runnable callback) {
showConfirmationAlert(activity,
notification == null ? R.string.sk_clear_all_notifications : R.string.sk_delete_notification,
notification == null ? R.string.sk_clear_all_notifications_confirm : R.string.sk_delete_notification_confirm,
notification == null ? R.string.sk_clear_all_notifications_confirm_action : R.string.sk_delete_notification_confirm_action,
notification == null ? R.drawable.ic_fluent_mail_inbox_dismiss_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DismissNotification(notification != null ? notification.id : null).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
callback.run();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
}).exec(accountID)
);
}
public static void setRelationshipToActionButton(Relationship relationship, Button button){
setRelationshipToActionButton(relationship, button, false);
}
@@ -591,7 +508,7 @@ public class UiUtils{
public void onSuccess(Relationship result) {
resultCallback.accept(result);
progressCallback.accept(false);
Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username), Toast.LENGTH_SHORT).show();
Toast.makeText(activity, activity.getString(result.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username), Toast.LENGTH_SHORT).show();
}
@Override
@@ -708,42 +625,6 @@ public class UiUtils{
return bitmap;
}
public static void insetPopupMenuIcon(Context context, MenuItem item) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
insetPopupMenuIcon(item, iconTint);
}
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) {
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint);
else icon.setTintList(iconTint);
icon=new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
item.setTitle(ssb);
}
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
if(menu.getClass().getSimpleName().equals("MenuBuilder")){
try {
Method m = menu.getClass().getDeclaredMethod(
"setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
m.invoke(menu, true);
enableMenuIcons(context, menu, asAction);
}
catch(Exception ignored){}
}
}
public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclude) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue;
insetPopupMenuIcon(item, iconTint);
}
}
public static void enablePopupMenuIcons(Context context, PopupMenu menu){
Menu m=menu.getMenu();
if(Build.VERSION.SDK_INT>=29){
@@ -755,158 +636,103 @@ public class UiUtils{
setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){}
}
enableMenuIcons(context, m);
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26){
item.setIconTintList(iconTint);
}else{
icon.setTintList(iconTint);
}
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
ssb.insert(0, " ");
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
item.setTitle(ssb);
}
}
public static void setUserPreferredTheme(Context context){
context.setTheme(switch (theme) {
case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
default -> trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
});
// boolean isDarkTheme = isDarkTheme();
switch(GlobalUserPreferences.color){
case PINK:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
case LIGHT ->
R.style.Theme_Mastodon_Light;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
});
break;
case PURPLE:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Original : R.style.Theme_Mastodon_AutoLightDark_Original;
case LIGHT ->
R.style.Theme_Mastodon_Light_Original;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Original : R.style.Theme_Mastodon_Dark_Original;
});
break;
case GREEN:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Green : R.style.Theme_Mastodon_AutoLightDark_Green;
case LIGHT ->
R.style.Theme_Mastodon_Light_Green;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Green : R.style.Theme_Mastodon_Dark_Green;
});
break;
case BLUE:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Blue : R.style.Theme_Mastodon_AutoLightDark_Blue;
case LIGHT ->
R.style.Theme_Mastodon_Light_Blue;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Blue : R.style.Theme_Mastodon_Dark_Blue;
});
break;
case ORANGE:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Orange : R.style.Theme_Mastodon_AutoLightDark_Orange;
case LIGHT ->
R.style.Theme_Mastodon_Light_Orange;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Orange : R.style.Theme_Mastodon_Dark_Orange;
});
break;
case YELLOW:
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack_Yellow : R.style.Theme_Mastodon_AutoLightDark_Yellow;
case LIGHT ->
R.style.Theme_Mastodon_Light_Yellow;
case DARK ->
GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack_Yellow : R.style.Theme_Mastodon_Dark_Yellow;
});
break;
}
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
if (palette != null) palette.apply(context);
}
public static boolean isDarkTheme(){
if(theme==GlobalUserPreferences.ThemePreference.AUTO)
if(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
return theme==GlobalUserPreferences.ThemePreference.DARK;
return GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK;
}
// https://mastodon.foo.bar/@User
// https://mastodon.foo.bar/@User/43456787654678
// https://pleroma.foo.bar/users/User
// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://friendica.foo.bar/profile/user
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
// https://pixelfed.social/p/connyduck/391263492998670833
// https://pixelfed.social/connyduck
// https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2
// https://gts.foo.bar/@goblin
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
//
// COPIED FROM https://github.com/tuskyapp/Tusky/blob/develop/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
public static boolean looksLikeMastodonUrl(String urlString) {
URI uri;
try {
uri = new URI(urlString);
} catch (URISyntaxException e) {
return false;
}
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null) return false;
String it = uri.getPath();
return it.matches("^/@[^/]+$") ||
it.matches("^/@[^/]+/\\d+$") ||
it.matches("^/users/\\w+$") ||
it.matches("^/notice/[a-zA-Z0-9]+$") ||
it.matches("^/objects/[-a-f0-9]+$") ||
it.matches("^/notes/[a-z0-9]+$") ||
it.matches("^/display/[-a-f0-9]+$") ||
it.matches("^/profile/\\w+$") ||
it.matches("^/p/\\w+/\\d+$") ||
it.matches("^/\\w+$") ||
it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$") ||
it.matches("^/users/[^/]+/statuses/[a-zA-Z0-9]+$") ||
it.matches("^/o/[a-f0-9]+$");
}
public static String getInstanceName(String accountID) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
}
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s->!s.getID().equals(exceptFor)).collect(Collectors.toList());
AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
.setItems(
sessions.stream().map(AccountSession::getFullUsername).toArray(String[]::new),
(dialog, which) -> sessionConsumer.accept(sessions.get(which))
)
.setTitle(titleRes == 0 ? R.string.choose_account : titleRes)
.setIcon(iconRes);
if (transformDialog != null) transformDialog.accept(builder);
builder.show();
}
@FunctionalInterface
public interface InteractionPerformer {
void interact(StatusInteractionController ic, Status status, Consumer<Status> resultConsumer);
}
public static void pickInteractAs(Context context, String accountID, Status sourceStatus, Predicate<Status> checkInteracted, InteractionPerformer interactionPerformer, @StringRes int interactAsRes, @StringRes int interactedAsAccountRes, @StringRes int alreadyInteractedRes, @DrawableRes int iconRes) {
pickAccount(context, accountID, interactAsRes, iconRes, session -> {
lookupStatus(context, sourceStatus, session.getID(), accountID, status -> {
if (checkInteracted.test(status)) {
Toast.makeText(context, alreadyInteractedRes, Toast.LENGTH_SHORT).show();
return;
}
StatusInteractionController ic = AccountSessionManager.getInstance().getAccount(session.getID()).getRemoteStatusInteractionController();
interactionPerformer.interact(ic, status, s -> {
if (checkInteracted.test(s)) {
Toast.makeText(context, context.getString(interactedAsAccountRes, session.getFullUsername()), Toast.LENGTH_SHORT).show();
}
});
});
}, null);
}
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> statusConsumer) {
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
statusConsumer.accept(queryStatus);
return;
}
new GetSearchResults(queryStatus.url, GetSearchResults.Type.STATUSES, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0));
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else {
dialog.setTitle(R.string.sk_loading_fediverse_resource_title);
}
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), (d, which) -> d.cancel());
if (url != null) {
dialog.setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.open_in_browser), (d, which) -> {
d.cancel();
launchWebBrowser(context, url);
});
}
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser){
public static void openURL(Context context, @Nullable String accountID, String url){
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())){
if(accountID!=null && "https".equals(uri.getScheme()) && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
List<String> path=uri.getPathSegments();
// Match URLs like https://mastodon.social/@Gargron/108132679274083591
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$")){
new GetStatusByID(path.get(1))
.setCallback(new Callback<>(){
@Override
@@ -920,88 +746,14 @@ public class UiUtils{
@Override
public void onError(ErrorResponse error){
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
launchWebBrowser(context, url);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
} else if (looksLikeMastodonUrl(url)) {
new GetSearchResults(url, null, true)
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Bundle args=new Bundle();
args.putString("account", accountID);
if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
Nav.go((Activity) context, ThreadFragment.class, args);
} else if (!results.accounts.isEmpty()) {
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
Nav.go((Activity) context, ProfileFragment.class, args);
} else {
if (launchBrowser) launchWebBrowser(context, url);
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.wrapProgress((Activity)context, R.string.loading, true)
.exec(accountID);
return;
}
}
launchWebBrowser(context, url);
}
public static void copyText(View v, String text) {
Context context = v.getContext();
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
}
v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
private static String getSystemProperty(String key){
try{
Class<?> props=Class.forName("android.os.SystemProperties");
Method get=props.getMethod("get", String.class);
return (String)get.invoke(null, key);
}catch(Exception ignore){}
return null;
}
public static boolean isMIUI(){
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
}
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText){
Bundle args = new Bundle();
if (prefilledText != null) args.putString("prefilledText", prefilledText);
return pickAccountForCompose(activity, accountID, args);
}
public static boolean pickAccountForCompose(Activity activity, String accountID){
return pickAccountForCompose(activity, accountID, (String) null);
}
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args){
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) {
UiUtils.pickAccount(activity, accountID, 0, 0, session -> {
args.putString("account", session.getID());
Nav.go(activity, ComposeFragment.class, args);
}, null);
return true;
} else {
return false;
}
}
}

View File

@@ -1,59 +0,0 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.Button;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.DrawableRes;
import me.grishka.appkit.utils.V;
public class FilterChipView extends Button{
private boolean currentlySelected;
public FilterChipView(Context context){
this(context, null);
}
public FilterChipView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public FilterChipView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
setCompoundDrawablePadding(V.dp(8));
setBackgroundResource(R.drawable.bg_filter_chip);
setTextAppearance(R.style.m3_label_large);
setTextColor(getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface)));
updatePadding();
}
@Override
protected void drawableStateChanged(){
super.drawableStateChanged();
if(currentlySelected==isSelected())
return;
currentlySelected=isSelected();
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()) : null;
Drawable end=getCompoundDrawablesRelative()[2];
setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null);
updatePadding();
}
private void updatePadding(){
int vertical=V.dp(6);
Drawable[] drawables=getCompoundDrawablesRelative();
setPaddingRelative(V.dp(drawables[0]==null ? 16 : 8), vertical, V.dp(drawables[2]==null ? 16 : 8), vertical);
}
public void setDrawableEnd(@DrawableRes int drawable){
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, drawable, 0);
updatePadding();
}
}

View File

@@ -1,320 +0,0 @@
package org.joinmastodon.android.ui.views;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.text.Editable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class FloatingHintEditTextLayout extends FrameLayout{
private EditText edit;
private TextView label;
private int labelTextSize;
private int offsetY;
private boolean hintVisible;
private Animator currentAnim;
private float animProgress;
private RectF tmpRect=new RectF();
private ColorStateList labelColors, origHintColors;
private boolean errorState;
public FloatingHintEditTextLayout(Context context){
this(context, null);
}
public FloatingHintEditTextLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public FloatingHintEditTextLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
if(isInEditMode())
V.setApplicationContext(context);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FloatingHintEditTextLayout);
labelTextSize=ta.getDimensionPixelSize(R.styleable.FloatingHintEditTextLayout_android_labelTextSize, V.dp(12));
offsetY=ta.getDimensionPixelOffset(R.styleable.FloatingHintEditTextLayout_editTextOffsetY, 0);
labelColors=ta.getColorStateList(R.styleable.FloatingHintEditTextLayout_labelTextColor);
ta.recycle();
setAddStatesFromChildren(true);
}
@Override
protected void onFinishInflate(){
super.onFinishInflate();
if(getChildCount()>0 && getChildAt(0) instanceof EditText et){
edit=et;
}else{
throw new IllegalStateException("First child must be an EditText");
}
label=new TextView(getContext());
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize);
// label.setTextColor(labelColors==null ? edit.getHintTextColors() : labelColors);
origHintColors=edit.getHintTextColors();
label.setText(edit.getHint());
label.setSingleLine();
label.setPivotX(0f);
label.setPivotY(0f);
label.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP);
lp.setMarginStart(edit.getPaddingStart()+((LayoutParams)edit.getLayoutParams()).getMarginStart());
addView(label, lp);
hintVisible=edit.getText().length()==0;
if(hintVisible)
label.setAlpha(0f);
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
}
private void onTextChanged(Editable text){
if(errorState){
errorState=false;
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field));
refreshDrawableState();
}
boolean newHintVisible=text.length()==0;
if(newHintVisible==hintVisible)
return;
if(currentAnim!=null)
currentAnim.cancel();
hintVisible=newHintVisible;
label.setAlpha(1);
edit.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
edit.getViewTreeObserver().removeOnPreDrawListener(this);
float scale=edit.getLineHeight()/(float)label.getLineHeight();
float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
AnimatorSet anim=new AnimatorSet();
if(hintVisible){
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(label, SCALE_X, scale),
ObjectAnimator.ofFloat(label, SCALE_Y, scale),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY),
ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 0f)
);
edit.setHintTextColor(0);
}else{
label.setScaleX(scale);
label.setScaleY(scale);
label.setTranslationY(transY);
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY),
ObjectAnimator.ofFloat(label, SCALE_X, 1f),
ObjectAnimator.ofFloat(label, SCALE_Y, 1f),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f),
ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 1f)
);
}
anim.setDuration(150);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
anim.start();
anim.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentAnim=null;
if(hintVisible){
label.setAlpha(0);
edit.setHintTextColor(origHintColors);
}
}
});
currentAnim=anim;
return true;
}
});
}
@Keep
public void setAnimProgress(float progress){
animProgress=progress;
invalidate();
}
@Keep
public float getAnimProgress(){
return animProgress;
}
@Override
public void onDrawForeground(Canvas canvas){
if(getForeground()!=null && animProgress>0){
canvas.save();
float width=(label.getWidth()+V.dp(8))*animProgress;
float centerX=label.getLeft()+label.getWidth()/2f;
tmpRect.set(centerX-width/2f, label.getTop(), centerX+width/2f, label.getBottom());
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O)
canvas.clipOutRect(tmpRect);
else
canvas.clipRect(tmpRect, Region.Op.DIFFERENCE);
super.onDrawForeground(canvas);
canvas.restore();
}else{
super.onDrawForeground(canvas);
}
}
@Override
public void setForeground(Drawable foreground){
super.setForeground(new PaddedForegroundDrawable(foreground));
}
@Override
public Drawable getForeground(){
if(super.getForeground() instanceof PaddedForegroundDrawable pfd){
return pfd.wrapped;
}
return null;
}
@Override
protected void drawableStateChanged(){
super.drawableStateChanged();
if(label==null || errorState)
return;
ColorStateList color=labelColors==null ? origHintColors : labelColors;
label.setTextColor(color.getColorForState(getDrawableState(), 0xff00ff00));
}
public void setErrorState(){
if(errorState)
return;
errorState=true;
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field_error, getContext().getTheme()));
label.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error));
}
private class PaddedForegroundDrawable extends Drawable{
private final Drawable wrapped;
private PaddedForegroundDrawable(Drawable wrapped){
this.wrapped=wrapped;
wrapped.setCallback(new Callback(){
@Override
public void invalidateDrawable(@NonNull Drawable who){
invalidateSelf();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when){
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what){
unscheduleSelf(what);
}
});
}
@Override
public void draw(@NonNull Canvas canvas){
wrapped.draw(canvas);
}
@Override
public void setAlpha(int alpha){
wrapped.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
wrapped.setColorFilter(colorFilter);
}
@Override
public int getOpacity(){
return wrapped.getOpacity();
}
@Override
public boolean setState(@NonNull int[] stateSet){
return wrapped.setState(stateSet);
}
@Override
public int getLayoutDirection(){
return wrapped.getLayoutDirection();
}
@Override
public int getAlpha(){
return wrapped.getAlpha();
}
@Nullable
@Override
public ColorFilter getColorFilter(){
return wrapped.getColorFilter();
}
@Override
public boolean isStateful(){
return wrapped.isStateful();
}
@NonNull
@Override
public int[] getState(){
return wrapped.getState();
}
@NonNull
@Override
public Drawable getCurrent(){
return wrapped.getCurrent();
}
@Override
public void applyTheme(@NonNull Resources.Theme t){
wrapped.applyTheme(t);
}
@Override
public boolean canApplyTheme(){
return wrapped.canApplyTheme();
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
LayoutParams lp=(LayoutParams) edit.getLayoutParams();
wrapped.setBounds(bounds.left+lp.leftMargin-V.dp(12), bounds.top, bounds.right-lp.rightMargin+V.dp(12), bounds.bottom);
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,6 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import java.time.Instant;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -22,16 +21,6 @@ public class StatusFilterPredicate implements Predicate<Status>{
@Override
public boolean test(Status status){
if(status.filtered!=null){
if (status.filtered.isEmpty()){
return true;
}
boolean matches=status.filtered.stream()
.map(filterResult->filterResult.filter)
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
.anyMatch(filter->filter.filterAction==Filter.FilterAction.HIDE);
return !matches;
}
for(Filter filter:filters){
if(filter.matches(status))
return false;

Some files were not shown because too many files have changed in this diff Show More