Compare commits
237 Commits
v1.1.4+for
...
v1.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e10faeefc4 | ||
|
|
65dbbb3d61 | ||
|
|
fa69868ca1 | ||
|
|
9c18de7b90 | ||
|
|
61bd19f6ff | ||
|
|
ba0689aef7 | ||
|
|
ad54e6bb4b | ||
|
|
f15fcb43da | ||
|
|
f2557b7815 | ||
|
|
a2726f5b61 | ||
|
|
a30f5bdee8 | ||
|
|
4cef005286 | ||
|
|
58a05681fe | ||
|
|
2589faf499 | ||
|
|
a5bdf34289 | ||
|
|
09fdd7f492 | ||
|
|
519d8b887d | ||
|
|
a2f2263bf7 | ||
|
|
5b73b10b34 | ||
|
|
b7a4364a28 | ||
|
|
3f075aff7b | ||
|
|
f4c33a5970 | ||
|
|
809af0ec18 | ||
|
|
4ee640e072 | ||
|
|
1cbf310555 | ||
|
|
f1fdc8aa43 | ||
|
|
d696daece3 | ||
|
|
967bb09282 | ||
|
|
136d910b3b | ||
|
|
51eb48a455 | ||
|
|
6ee8afcf96 | ||
|
|
a59f2d4609 | ||
|
|
b75d871837 | ||
|
|
c72f93b990 | ||
|
|
586d337ead | ||
|
|
d84e10a22e | ||
|
|
351ec89207 | ||
|
|
7db7bf0220 | ||
|
|
a9764c4f46 | ||
|
|
a430b6a280 | ||
|
|
6a01124d13 | ||
|
|
2843e445e2 | ||
|
|
5c947d14b2 | ||
|
|
590adba3e3 | ||
|
|
efee249173 | ||
|
|
6d2ed27364 | ||
|
|
55716d742f | ||
|
|
e4555da735 | ||
|
|
8b4b99bec7 | ||
|
|
5de4b19969 | ||
|
|
a9460f401e | ||
|
|
012cca550e | ||
|
|
0c743db412 | ||
|
|
b819ee7d6d | ||
|
|
e7e3a249b5 | ||
|
|
980c580b55 | ||
|
|
e23c530e74 | ||
|
|
a64caccca2 | ||
|
|
726ec7159c | ||
|
|
e74256ef6f | ||
|
|
a18718ca81 | ||
|
|
5a9bc0e269 | ||
|
|
2d39c62ff0 | ||
|
|
0da4f79413 | ||
|
|
2bdef776a2 | ||
|
|
a57ad67308 | ||
|
|
e63d04cea9 | ||
|
|
cf48cb6f75 | ||
|
|
542e53cf6a | ||
|
|
bab1d40038 | ||
|
|
18f605e5c5 | ||
|
|
cd8a80a6a1 | ||
|
|
3ce8aa7894 | ||
|
|
b356794da9 | ||
|
|
afe8f6cf6a | ||
|
|
ed0df82fe9 | ||
|
|
d3bc7a9790 | ||
|
|
633c0f870d | ||
|
|
f9fe7819f9 | ||
|
|
f3d13545e7 | ||
|
|
f6b77777b5 | ||
|
|
340990fbd9 | ||
|
|
a7687f8e35 | ||
|
|
52aa4a5289 | ||
|
|
268accea14 | ||
|
|
101cde4d84 | ||
|
|
8863446f6a | ||
|
|
28a0824f6b | ||
|
|
4b16262a1a | ||
|
|
b1f9d0516d | ||
|
|
10e7cbf022 | ||
|
|
531b8ead04 | ||
|
|
4b2c94ab52 | ||
|
|
5b21747d5d | ||
|
|
a98becf2f4 | ||
|
|
9fda48cff0 | ||
|
|
54f9eace67 | ||
|
|
0e6f3df212 | ||
|
|
a8c3f1555e | ||
|
|
cd797a637b | ||
|
|
53b2eb59d3 | ||
|
|
09e2224596 | ||
|
|
5999aad21b | ||
|
|
874ce07c3e | ||
|
|
1787d08718 | ||
|
|
9a12be88da | ||
|
|
8f6bb74e61 | ||
|
|
e4c9eb089a | ||
|
|
0e635aec23 | ||
|
|
dc90c09cea | ||
|
|
06cb335a0a | ||
|
|
e67bd2972a | ||
|
|
5a681d3557 | ||
|
|
4200486aeb | ||
|
|
62411a563f | ||
|
|
2cabe94ba0 | ||
|
|
4a6baae97a | ||
|
|
bb12a66781 | ||
|
|
de5929d8d2 | ||
|
|
d7699ef079 | ||
|
|
3ab04ebca8 | ||
|
|
78d2aa96d7 | ||
|
|
3e903f4a1d | ||
|
|
353b1873cd | ||
|
|
f4de7d18f3 | ||
|
|
5dbac5fc6b | ||
|
|
172d44997f | ||
|
|
57b0b04c00 | ||
|
|
ca9ce43b07 | ||
|
|
ef41122aca | ||
|
|
9e7676b62a | ||
|
|
56492c07f5 | ||
|
|
ce9fabd406 | ||
|
|
0dd6c43117 | ||
|
|
5159aab19c | ||
|
|
4e2cf247e9 | ||
|
|
9df02d9857 | ||
|
|
bb4a5202d7 | ||
|
|
2320014eb3 | ||
|
|
3692c2b205 | ||
|
|
9facdb938d | ||
|
|
7856858aea | ||
|
|
d0328957f5 | ||
|
|
4d868cc5aa | ||
|
|
ca9e515bd5 | ||
|
|
524c0d607b | ||
|
|
df1d451e82 | ||
|
|
2c61551e5c | ||
|
|
158af27309 | ||
|
|
6e8542e33b | ||
|
|
187693883c | ||
|
|
9017d00541 | ||
|
|
9182bd1a15 | ||
|
|
5cdd726d21 | ||
|
|
d00fbe074b | ||
|
|
365fac5efe | ||
|
|
1d60031f4c | ||
|
|
2c7ed4be3e | ||
|
|
3b9d4d3f9d | ||
|
|
78824fa425 | ||
|
|
e271a4a330 | ||
|
|
b898dc010e | ||
|
|
de369633ec | ||
|
|
3f075eab13 | ||
|
|
3fb063bee4 | ||
|
|
c43dd5aa49 | ||
|
|
0285158edc | ||
|
|
9a6a3422fb | ||
|
|
523eb70ca6 | ||
|
|
b57972ae0f | ||
|
|
37598df24e | ||
|
|
f04df2d2c4 | ||
|
|
881762852e | ||
|
|
5d056d5bea | ||
|
|
f500cc7ebf | ||
|
|
8d6eb0f810 | ||
|
|
94de724d4e | ||
|
|
555e8838d4 | ||
|
|
58beb73595 | ||
|
|
865f66aa30 | ||
|
|
82e95010a2 | ||
|
|
68a053cb76 | ||
|
|
2467017382 | ||
|
|
d3a7faba51 | ||
|
|
eb7574d282 | ||
|
|
689328931a | ||
|
|
d3a2ce0a57 | ||
|
|
50a092a2c4 | ||
|
|
a852b66d94 | ||
|
|
109d967f2d | ||
|
|
752435857d | ||
|
|
03e68ba56c | ||
|
|
ef37d8afc4 | ||
|
|
1ac390f6ee | ||
|
|
586d04f311 | ||
|
|
6e81469b45 | ||
|
|
a8431a498d | ||
|
|
a2dca57eb5 | ||
|
|
fdfb0a377d | ||
|
|
f5a9a11032 | ||
|
|
32c6fc9a59 | ||
|
|
b87b086dfa | ||
|
|
caa77a9c54 | ||
|
|
ecf0c2b173 | ||
|
|
8f7ef0d564 | ||
|
|
d8e9bbd6b1 | ||
|
|
174029376c | ||
|
|
8e06f72064 | ||
|
|
f75a7e793d | ||
|
|
a395b82c85 | ||
|
|
699f36bf1a | ||
|
|
ef57b7425d | ||
|
|
c21b2b6a43 | ||
|
|
c5d4318d57 | ||
|
|
7a4621ef13 | ||
|
|
fa53a5ed4f | ||
|
|
f2ec2c5333 | ||
|
|
49e005eb84 | ||
|
|
ad9b19646d | ||
|
|
80336c7fae | ||
|
|
996c91420c | ||
|
|
48396344e3 | ||
|
|
0a8c61226e | ||
|
|
e64d8ccf09 | ||
|
|
9791121392 | ||
|
|
06c5002aa5 | ||
|
|
fcdbc2bc8d | ||
|
|
749511630a | ||
|
|
4befc0c045 | ||
|
|
8b2287fa84 | ||
|
|
3272b7553b | ||
|
|
00f7cff402 | ||
|
|
87f45a74f0 | ||
|
|
0cd0fe1952 | ||
|
|
458effc27c | ||
|
|
4422b774b7 | ||
|
|
68a9eba868 |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: mastodon
|
||||
open_collective: # Replace with a single Open Collective username e.g., user1
|
||||
ko_fi: # Replace with a single Ko-fi username e.g., user1
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username e.g., user1
|
||||
issuehunt: # Replace with a single IssueHunt username e.g., user1
|
||||
otechie: # Replace with a single Otechie username e.g., user1
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
103
README.md
103
README.md
@@ -1,102 +1,17 @@
|
||||

|
||||
Mastodon for Android
|
||||
======================
|
||||
|
||||
# Mastodos
|
||||
[](https://crowdin.com/project/mastodon-for-android)
|
||||
|
||||
> A fork of the [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly won’t ever be implemented, such as the federated timeline, unlisted posting, bookmarks and an image description viewer.
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
|
||||
|
||||
[](https://github.com/sk22/mastodos/releases/latest/download/mastodos.apk)
|
||||
This is the repository for the official Android app for Mastodon.
|
||||
|
||||
---
|
||||
## Contributing
|
||||
|
||||
## Key features
|
||||
Our goal is delivering a polished, professionally designed and user-friendly app. We proceed according to wireframes provided by a professional UX designer that works with Mastodon gGmbH. This means that any outside contributions that change the app visually must first be coordinated with the UX designer. *This can take time.* Furthermore, we work off of an internal roadmap and aim for feature-parity and consistency with our iOS app. The iOS app is designated as the "primary" between the two, therefore, if you want to request features, please do so in the [Mastodon for iOS](https://github.com/mastodon/mastodon-ios) repository, as you are requesting a feature to be both in iOS and Android (exceptions being system integrations specific to Android). On the other hand, any contributions that improve existing functionality, performance, or accessibility should not have any roadblocks to being merged.
|
||||
|
||||
### **Unlisted posting**
|
||||
|
||||
**Allows you to post publicly without having your post show up in trends, hashtags or public timelines (i.e., in the tabs “Local”, “Community” and “Posts”).**
|
||||
|
||||
When posting with Unlisted visibility, your posts will still be publicly accessible in your profile. They will also be shown in people’s Home timelines, but only if they follow you or someone they follow reposted/replied to your post.
|
||||
|
||||
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).
|
||||
|
||||
### **Federated timeline**
|
||||
|
||||
**This allows you to chronologically see all Public posts from people on all other Fediverse instances your home instance is connected to.**
|
||||
|
||||
Despite being one of the main features of federated social media, the Federated timeline wasn’t included in the official Mastodon app – supposedly, because this conflicts with Google’s safety requirements for apps on the Play Store.
|
||||
|
||||
That’s one of the reasons why choosing a small, **well-moderated instance is important**. Instance admins and moderators should always make sure to ban abusive users and stop federating with instances who platform them. On well-moderated instances, the Federated timeline can be a welcoming place to meet new people!
|
||||
|
||||
### **Image description viewer**
|
||||
|
||||
**Allows you to quickly check whether an image or video has an alternative text attached to it.**
|
||||
|
||||
This is important to **ensure the content you’re sharing is as accessible as possible** to people who can’t see the images and rely on software to read back the provided content descriptions. Thankfully, it’s quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
|
||||
|
||||
### **Pinning posts**
|
||||
|
||||
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in people’s profiles shows all the posts they pinned.**
|
||||
|
||||
On the Fediverse, it’s quite common for people to pin posts they want others to read before following them. You can pin/unpin posts yourself by clicking the `⋯` button in the top right corner of your posts.
|
||||
|
||||
### **Bookmarks**
|
||||
|
||||
**They allow for quickly saving posts and viewing them through the Bookmarks button on the top right of your profile.**
|
||||
|
||||
To bookmark a post, press the button between the Favorite and Share buttons on the bottom of the post. Bookmarks are saved privately, so the post authors won’t know you saved their post – the list of bookmarked posts is only visible to you.
|
||||
|
||||
## Installation
|
||||
|
||||
**Press the download button above to download the APK. Open the downloaded file on your Android device to install it. Mastodos will automatically notify you about new updates inside the app.**
|
||||
|
||||
To install this app on your Android device, download the [latest release from GitHub](https://github.com/sk22/mastodos/releases/latest/download/mastodos.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/mastodos/releases) page.
|
||||
|
||||
Mastodos makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)’s automatic update checker. Mastodos 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!
|
||||
|
||||
---
|
||||
|
||||
## Detailed changes
|
||||
|
||||
### Features
|
||||
|
||||
* [Add “Unlisted” as a post visibility option](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
|
||||
* [Add “Federation” tab and change Discover tab order](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/add-federated-timeline) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/8))
|
||||
* [Add image description button and viewer](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
|
||||
* [Implement pinning posts and displaying pinned posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
|
||||
* [Implement deleting and re-drafting](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/delete-redraft) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/21))
|
||||
* [Implement a bookmark button and list](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos: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:mastodos:feature/check-for-update-button)
|
||||
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/mark-media-as-sensitive)
|
||||
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
|
||||
* [Follow and unfollow hashtags](https://github.com/sk22/mastodos/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
|
||||
* [Notification bell for posts](https://github.com/sk22/mastodos/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:mastodos:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
|
||||
* [List favorited posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/favs-list)
|
||||
* [Accept/reject follow requests](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/follow-requests)
|
||||
* [Display content warning title above text](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/cw-above-text)
|
||||
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/posts-notifications-tab)
|
||||
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/display-reply-visibility)
|
||||
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:true-black-improvements)
|
||||
|
||||
### Behavior
|
||||
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/back-returns-home) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/118))
|
||||
* [Always preserve content warnings when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/always-preserve-cw) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/113))
|
||||
* [Display full image when adding image description](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
|
||||
* [Set spoiler height independently to content height](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
|
||||
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:compact-extended-footer)
|
||||
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:settings/hide-interaction-numbers)
|
||||
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:feature/cw-above-text)
|
||||
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:mastodos:settings/disable-marquee)
|
||||
|
||||
### Branding
|
||||
|
||||
* App name “Mastodos”
|
||||
* Pink primary color
|
||||
* Custom icon: Modulate upstream icon using ImageMagick
|
||||
```bash
|
||||
mogrify -modulate 90,100,140 mastodon/src/main/res/mipmap-*/ic_launcher*.png
|
||||
```
|
||||
If you would like to help translate the app into your language, please go to [Crowdin](https://crowdin.com/project/mastodon-for-android). If your language is not listed in the Crowdin project, please create an issue and we will add it. Please do not create pull requests that modify `strings.xml` files for languages other than English.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -109,3 +24,5 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
|
||||
## License
|
||||
|
||||
This project is released under the [GPL-3 License](./LICENSE).
|
||||
|
||||
The Mastodon name and logo are trademarks of Mastodon gGmbH. If you intend to redistribute a modified version of this app, use a unique name and icon for your app that does not mistakenly imply any official connection with or endorsement by Mastodon gGmbH.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
title: Mastodos
|
||||
theme: minima
|
||||
16
fastlane/metadata/android/be-BY/full_description.txt
Normal file
16
fastlane/metadata/android/be-BY/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
More features:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
1
fastlane/metadata/android/be-BY/short_description.txt
Normal file
1
fastlane/metadata/android/be-BY/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralized social network
|
||||
1
fastlane/metadata/android/be-BY/title.txt
Normal file
1
fastlane/metadata/android/be-BY/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon je největší decentralizovanou sociální sítí na internetu. Místo jednné webové stránky je to síť pro miliony uživatelů v nezávislých komunitách, kteří mohou všichni vzájemně a bezproblémově komunikovat. Bez ohledu na to, co vás baví, můžete se setkat s vášnivými lidmi, kteří o tom vysílají na Mastodon!
|
||||
Mastodon je největší decentralizovanou sociální sítí na internetu. Místo jediné webové stránky je to síť pro miliony uživatelů v nezávislých komunitách, ve kterých mohou všichni vzájemně a bezproblémově komunikovat. Bez ohledu na to, co vás baví, můžete se setkat s vášnivými lidmi, kteří o tom přispívají na Mastodon!
|
||||
|
||||
Připojte se ke komunitě a vytvořte svůj profil. Najděte a sledujte fascinující lidi a přečtěte si jejich příspěvky v bezreklamní a chronologické časové linii. Vyjádřete se pomocí vlastních emojí, obrázků, GIFů, videí a zvuku v 500-znakových příspěvcích. Odpovězte na vlákna a reblogujte příspěvky od kohokoliv, abyste mohli sdílet skvělé věci. Najděte nové účty pro sledování a populární hashtagy pro rozšíření vaší sítě.
|
||||
Připojte se ke komunitě a vytvořte svůj profil. Najděte a sledujte fascinující lidi a přečtěte si jejich příspěvky v chronologické časové ose bez reklam. Vyjádřete se pomocí vlastních emoji, obrázků, GIFů, videí a zvuku v 500-znakových příspěvcích. Odpovězte na vlákna a boostujte příspěvky od kohokoliv, abyste mohli sdílet skvělé věci. Najděte nové účty pro sledování a populární hashtagy pro rozšíření vaší sítě.
|
||||
|
||||
Mastodon je postaven se zaměřením na soukromí a bezpečnost. Rozhodněte, zda jsou vaše příspěvky sdíleny se svými sledujícími, jen s lidmi, které zmiňujete, nebo s celým světem. Upozornění na obsah vám umožní skrýt příspěvky obsahující citlivý nebo spouštěcí materiál, dokud se s nimi nezačnete zabývat. Každá komunita má vlastní pokyny a moderátory, aby udržela své členy v bezpečí, a robustní blokování a hlášení nástrojů pomáhá předcházet zneužití.
|
||||
Mastodon je postaven se zaměřením na soukromí a bezpečnost. Rozhodněte, zda jsou vaše příspěvky sdíleny se vašimi sledujícími, jen s lidmi, které zmíníte, nebo s celým světem. Upozornění na obsah vám umožní skrýt příspěvky obsahující citlivý nebo spouštěcí materiál, dokud se s nimi nezačnete zabývat. Každá komunita má vlastní pokyny a moderátory, aby udržela své členy v bezpečí, a robustní blokování a nahlašovací nástroje pomáhácí předcházení zneužití.
|
||||
|
||||
Další funkce:
|
||||
Více funkcí:
|
||||
|
||||
• Tmavý režim: Čtěte příspěvky ve světlém, tmavém nebo zcela černém režimu
|
||||
• Ankety: Požádejte sledující o jejich názor a spojte se s jejich hlasováním
|
||||
• Průzkum: Trendové hashtagy a účty jsou pryč na jedno klepnutí
|
||||
• Upozornění: Dostávejte upozornění na nové sledování, odpovědi a reblogy
|
||||
• Tmavý režim: Čtěte příspěvky ve světlém, tmavém nebo pravém černém režimu
|
||||
• Ankety: Požádejte sledující o jejich názor a sečtěte jejich hlasy
|
||||
• Objevit: Populární hashtagy a účty jsou pryč na jedno klepnutí
|
||||
• Oznámení: Dostávejte oznámení o nových sledujících, odpovědích a boostech
|
||||
• Sdílení: Odesílání přímo do Mastodonu z libovolného seznamu sdílení v jakékoliv aplikaci
|
||||
• Roztomilost: Naším maskotem je roztomilý slon, kterého čas od času uvidíte
|
||||
|
||||
Mastodon je registrovaný neziskový projekt a vývojový program je podporován přímo vašimi dary. Neexistuje žádná reklama, žádná monetizace a žádný rizikový kapitál a my máme v plánu to udržet.
|
||||
Mastodon je registrovaný neziskový projekt a vývojový program je podporován přímo vašimi dary. Neexistuje žádná reklama, žádná monetizace a žádný rizikový kapitál a máme v plánu to udržet.
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Website ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal was dich interessiert, auf Mastodon kannst du interessierte Leute treffen, die darüber schreiben!
|
||||
Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Webseite ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal, was du magst, auf Mastodon kannst du begeisterte Menschen treffen, die darüber schreiben!
|
||||
|
||||
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten, und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit benutzerdefinierten Emojis, Bildern, GIFs, Videos und Audio in 500-Zeichen-Beiträgen aus. Antworte auf Threads und teile Beiträge von anderen, um großartige Sachen zu verbreiten. Finde neue Accounts zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
||||
Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit eigenen Emojis, Bildern, GIFs, Videos und Klängen in 500-Zeichen-Beiträgen aus. Antworte auf Themen und teile Beiträge von anderen, um tolle Dinge zu verbreiten. Finde neue Konten zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern.
|
||||
|
||||
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder triggerndem Inhalt ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen.
|
||||
Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder bedenklichen Inhalten ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen.
|
||||
|
||||
Weitere Funktionen:
|
||||
|
||||
• Dunkler Modus: Beiträge im hellen, dunklen oder schwarzen Modus lesen
|
||||
• Umfragen: Frage deine Follower nach ihrer Meinung und zähle die Stimmen
|
||||
• Umfragen: frage deine Follower nach ihrer Meinung und zähle die Stimmen
|
||||
• Entdecken: trendende Hashtags und Profile sind nur einen Fingertipp entfernt
|
||||
• Benachrichtigungen: Erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
|
||||
• Teilen: Veröffentliche auf Mastodon aus jeder beliebigen anderen App
|
||||
• Niedlichkeit: Unser Maskottchen ist ein entzückender Elefant, und du wirst ihn von Zeit zu Zeit auftauchen sehen
|
||||
• Benachrichtigungen: erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge
|
||||
• Teilen: veröffentliche auf Mastodon aus jeder beliebigen anderen App
|
||||
• Niedlichkeit: unser Maskottchen ist ein entzückender Elefant und du wirst ihn von Zeit zu Zeit auftauchen sehen
|
||||
|
||||
Mastodon ist eine eingetragene gemeinnützige Organisation, und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetisierung und kein Venture-Capital, und wir planen, das auch so beizubehalten.
|
||||
Mastodon ist eine eingetragene gemeinnützige Organisation und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetarisierung und kein Risikokapital und so soll es auch bleiben.
|
||||
@@ -1,6 +1,6 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
@@ -13,4 +13,4 @@ More features:
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
|
||||
16
fastlane/metadata/android/fil-PH/full_description.txt
Normal file
16
fastlane/metadata/android/fil-PH/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
More features:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
1
fastlane/metadata/android/fil-PH/short_description.txt
Normal file
1
fastlane/metadata/android/fil-PH/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralized social network
|
||||
1
fastlane/metadata/android/fil-PH/title.txt
Normal file
1
fastlane/metadata/android/fil-PH/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
16
fastlane/metadata/android/ga-IE/full_description.txt
Normal file
16
fastlane/metadata/android/ga-IE/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
More features:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
1
fastlane/metadata/android/ga-IE/short_description.txt
Normal file
1
fastlane/metadata/android/ga-IE/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralized social network
|
||||
1
fastlane/metadata/android/ga-IE/title.txt
Normal file
1
fastlane/metadata/android/ga-IE/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
’S e an lìonra sòisealta sgaoilte as motha air an eadar-lìon a th’ ann am Mastodon. Seach aon làrach-lìn a-mhàin, ’s e lìonra de mhilleanan de dhaoine ann an coimhearsnachdan neo-eisimeileach a th’ ann agus ’s urrainn dhan a h-uile duine bruidhinn ri chèile fhathast gun duilgheadas. Ge b’ e dè na rudan a tha ùidh agad annta, coinnichidh tu ri daoine a sgrìobhas mun dèidhinn air Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Faigh ballrachd ann an coimhearsnachd ’s cruthaich pròifil dhut. Lorg is lean daoine inntinneach agus leugh na postaichean aca air loidhne-ama cheart gun sanasachd. Cuir thu fhèin an cèill le Emojis gnàthaichte, dealbhan, GIFs, videothan is fuaimean ann am postaichean le 500 caractar. Freagair ri snàithleanan is brosnaich postaichean le neach sam bith airson deagh rudan a cho-roinneadh. Lorg cunntasan ùra ri leantainn is tagaichean hais a’ treandadh airson an lìonra agad a leudachadh.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Chaidh Mastodon a thogail leis an aire air prìobhaideachd is sàbhailteachd. Tha e an urra riut fhèin an co-roinn thu post leis an luchd-leantainn agad, leis na daoine air an doir thu iomradh a-mhàin no leis an t-saoghal mhòr. Leigidh rabhaidhean susbainte leat postaichean sa bheil susbaint fhrionasach fhalach is cha leig daoine leas coimhead air ach nuair a bhios iad deònach. Tha riaghailtean is maoir fa leth aig gach coimhearsnachd airson a buill a chumail sàbhailte agus cuidichidh innealan bacaidh is gearain le dìon o dhroch-dhìol.
|
||||
|
||||
More features:
|
||||
Gleusan eile:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Modh dorcha: Leugh postaichean le modh soilleir, dorcha no dubh dorcha
|
||||
• Cunntasan-bheachd: Faighnich dhen luchd-leantainn dè am beachd is faigh cunntas nam bhòt
|
||||
• Rùraich: Ruig tagaichean hais is cunntasan a’ treandadh le aon ghnogag
|
||||
• Brathan: Faigh brathan mu luchd-leantainn, freagairtean is brosnachaidhean ùra
|
||||
• Co-roinn: Postaich gu Mastodon gu dìreach o shiota co-roinnidh ann an aplacaid sam bith
|
||||
• Stampachd: ’S e ailbhean ealanta a tha san t-suaichnean againn is nochdaidh e o àm gu àm
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
’S e bhuidheann neo-phrothaideach clàraichte a th’ ann am Mastodon a gheibh taic dhìreach o na tabhartasan agad. Chan eil sanasachd, airgeadachadh no calpa iomairte sam bith ann agus tha fainear dhuinn ’ga chumail mar sin.
|
||||
@@ -1 +1 @@
|
||||
Decentralized social network
|
||||
Lìonra sòisealta sgaoilte
|
||||
16
fastlane/metadata/android/hu-HU/full_description.txt
Normal file
16
fastlane/metadata/android/hu-HU/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
A Mastodon a legnagyobb decentralizált közösségi hálózat az interneten. Egyetlen weboldal helyett, ez több millió felhasználóból álló, független közösségek hálózata, amelyek egymással kapcsolatba tudnak lépni, zökkenőmentesen. Nem számít, mi a hobbid, a Mastodonon találkozhatsz róla posztoló lelkes emberekkel!
|
||||
|
||||
Csatlakozz egy közösséghez és készítsd el a profilodat. Keress és kövess lenyűgöző embereket, és olvasd egy reklámmentes, kronologikus idővonalon a bejegyzéseiket. Fejezd ki magad egyedi hangulatjelekkel, képekkel, GIFekkel, videókkal és hanggal, 500 karakter hosszúságú posztokban. Reply to threads and reblog posts from anyone to share great stuff. Fedezz fel új fiókokat amiket követhetsz és felkapott hashtageket, hogy bővíthesd a kapcsolataidat.
|
||||
|
||||
A Mastodon az adatvédelemre és a biztonságra összpontosítva épült. Döntsd el, hogy a posztjaidat csak a követőiddel, csak azokkal akiket megemlítesz, vagy az egész világgal osztod meg. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
|
||||
More features:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
1
fastlane/metadata/android/hu-HU/short_description.txt
Normal file
1
fastlane/metadata/android/hu-HU/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralizált szociális hálózat
|
||||
1
fastlane/metadata/android/hu-HU/title.txt
Normal file
1
fastlane/metadata/android/hu-HU/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon adalah jejaring sosial terdesentralisasi terbesar di internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon adalah jejaring sosial terdesentralisasi terbesar di internet. Daripada sebuah satu situs web, ini adalah jaringan dari jutaan pengguna dalam komunitas tersendiri yang dapat berinteraksi antar sesama, tanpa masalah. Tanpa memedulikan apa yang Anda minat, Anda dapat bertemu orang-orang yang mengirimkan apa yang mereka minat di Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Temukan akun-akun baru untuk diikuti dan hashtag yang sedang tren untuk memperluas jejaring Anda.
|
||||
Bergabung sebuah komunitas dan buat profil Anda. Temukan dan ikuti orang-orang menarik dan lihat kiriman mereka dalam linimasa kronologis tanpa iklan. Ekspresikan diri Anda dengan emoji kustom, gambar, GIF, video, dan audio dalam kiriman dengan batasan 500 karakter. Balas ke utasan dan bagikan kiriman dari siapa pun ke pengikut Anda untuk membagikan hal-hal yang keren. Temukan akun baru untuk diikuti dan tagar yang sedang tren untuk memperluas jejaring Anda.
|
||||
|
||||
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah postingan Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon dibuat dengan fokus pada privasi dan keamanan. Tentukan apakah kiriman Anda dibagikan kepada pengikut, hanya orang yang disebut, atau seluruh dunia. Peringatan konten memungkinkan Anda untuk menyembunyikan kiriman yang berisi material sensitif atau memicu sampai Anda siap untuk terlibat dengan mereka. Setiap komunitas memiliki pedoman dan moderator sendiri-sendiri untuk menjaga anggotanya aman, dan alat pemblokiran dan pelaporan yang kokoh membantu mencegah pelecehan.
|
||||
|
||||
Fitur lainnya:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Mode Gelap: Baca kiriman dalam mode terang, gelap, atau gelap asli
|
||||
• Pemungutan suara: Tanya pengikut tentang opini mereka dan hitung pilihannya
|
||||
• Jelajahi: Tagar dan akun tren dengan satu ketuk
|
||||
• Pemberitahuan: Dapatkan pemberitahuan tentang pengikut, balasan, dan pembagian baru
|
||||
• Pembagian: Kirim langsung ke Mastodon dari lembar pembagian apa pun dalam aplikasi apa pun
|
||||
• Kelucuan: Maskot kami adalah seekor gajah yang lucu, dan Anda akan melihat dia muncul dari waktu ke waktu
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon adalah nirlaba yang terdaftar dan pengembangan didukung secara langsung dari donasi Anda. Tanpa periklanan, tanpa monetisasi, dan tanpa kapitalisme ventura, dan kami berencana untuk tetap seperti itu.
|
||||
30
fastlane/metadata/android/is-IS/full_description.txt
Normal file
30
fastlane/metadata/android/is-IS/full_description.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
Mastodon er stærsta ómiðstýrða samfélagsnetið á internetinu. Í staðinn fyrir að vera á inu vefsvæði, er þetta net með milljónum notenda í
|
||||
sjálfstæðum samfélögum, sem geta óhindrað átt í samskiptum við hvern annan. Sama hvað þú ert að pæla, alltaf geturðu hitt áhugasamt fólk í gegnum
|
||||
færslur á Mastodon!
|
||||
|
||||
Taktu þátt í samfélagi og útbúðu notandasnið fyrir þig. Finndu og fylgstu með áhugaverðu fólki og lestu færslurnar þeirra á
|
||||
auglýsingalausri, raðaðri tímalínu. Tjáðu þig með sérsniðnum emoji-táknum, myndum, GIF-hreyfimyndum, myndskeiðum
|
||||
og hljóðskrám í 500-stafa færslum. Svaraðu spjallþráðum og endurbirtu færslur frá hverjum sem er til að deila
|
||||
frábæru efni. Finndu nýja notendur til að fylgjast með og skoðaðu vinsæl myllumerki til að
|
||||
útvíkka netið þitt.
|
||||
|
||||
Mastodon er byggt með áherslu á gagnaleynd og öryggi. Ákveddu hvort færslunum þínum sé deilt með þeim sem fylgjast með þér, aðeins
|
||||
fólkinu sem þú minnist á, eða allri veröldinni. Viðvaranir vegna efnis gera þér kleift að fela færslur sem innihalda
|
||||
viðkvæmt eða eldfimt efni þangað til þú ert í stuði til að eiga við slíkt. Hvert samfélag er með sínar eigin reglur og umsjónarmenn til að passa upp á
|
||||
öryggi meðlimanna, auk áreiðanlegra verkfæra til að útiloka aðila og
|
||||
meðhöndla kærur, sem hjálpar til við að koma í veg fyrir misnotkun.
|
||||
|
||||
Fleiri eiginleikar:
|
||||
|
||||
• Dökkur hamur: Lestu færslur í ljósum, dökkum eða sönnum kolsvörtum ham
|
||||
• Kannanir: Spyrðu fylgjendur um skoðanir þeirra og teldu atkvæðin
|
||||
• Uppgötva: Vinsæl myllumerki og notendaaðgangar eru við hendina
|
||||
• Tilkynningar: Fáðu tilkynningar um nýja fylgjendur, svör og endurbirtingar
|
||||
• Deiling: Birtu beint á Mastodon frá hvaða deilingarblaði sem er í hvaða
|
||||
forriti sem er
|
||||
• Krúttlegheit: Gæludýrið okkar er vinalegur loðfíll sem þú gætir rekist á
|
||||
öðru hverju
|
||||
|
||||
Mastodon er skráð óhagnaðardrifin sjálfseignarstofnun og er þróun þess
|
||||
drifin áfram með styrkjum frá þér. Það eru engar auglýsingar, engin gjaldtaka og engir áhættufjárfestar - við
|
||||
höfum hugsað okkur að halda því þannig.
|
||||
1
fastlane/metadata/android/is-IS/short_description.txt
Normal file
1
fastlane/metadata/android/is-IS/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Dreifstýrt samfélagsnet
|
||||
1
fastlane/metadata/android/is-IS/title.txt
Normal file
1
fastlane/metadata/android/is-IS/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon is het grootste gedecentraliseerde sociale netwerk op het internet. In plaats van één enkele website is het een netwerk van miljoenen gebruikers in onafhankelijke gemeenschappen die allemaal naadloos met elkaar kunnen communiceren. Waar je ook mee bezig bent, je kunt gepassioneerde mensen ontmoeten die erover berichten op Mastodon!
|
||||
|
||||
Word lid van een community en maak je profiel aan. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Antwoord op berichten en boost iedereens berichten om geweldige dingen te delen. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Word lid van een gemeenschap en maak je profiel aan. Vind en volg fascinerende mensen en lees hun berichten in een advertentievrije, chronologische tijdlijn. Druk jezelf uit met aangepaste emoji, afbeeldingen, GIF’s, video’s en audio in berichten van 500 karakters. Antwoord op berichten en boost iedereens berichten om geweldige dingen te delen. Vind nieuwe accounts om te volgen en hashtags om je netwerk uit te breiden.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon is gebouwd met een focus op privacy en veiligheid. Bepaal zelf of je berichten met je volgers, alleen de mensen die je noemt, of de hele wereld worden gedeeld. Inhoudswaarschuwingen laten je berichten verbergen die gevoelig of aanmatigend materiaal bevatten, totdat je er klaar voor bent om ermee ze te bekijken. Elke gemeenschap heeft haar eigen richtlijnen en moderators om haar leden veilig te houden, en robuuste blokkerings- en rapportagetools helpen misbruik te voorkomen.
|
||||
|
||||
Meer mogelijkheden:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Donkere Modus: Berichten lezen in licht, donker of echt zwart
|
||||
• Polls: Vraag volgers om hun mening en tel de stemmen
|
||||
• Ontdekken: Trending hashtags en accounts zijn een tik weg
|
||||
• Meldingen: Krijg een melding over nieuwe volgers, reacties en boosts
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Delen: Deel vanuit elke app direct op Mastodon
|
||||
• Schattigheid: Onze mascotte is een schattige olifant, en je zult ze van tijd tot tijd zien verschijnen
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon is een geregistreerde non-profit en de ontwikkeling wordt direct ondersteund door jouw donaties. Er is geen reclame, geen geldelijk gewin en geen durfkapitaal en we zijn van plan het zo te houden.
|
||||
16
fastlane/metadata/android/ro-RO/full_description.txt
Normal file
16
fastlane/metadata/android/ro-RO/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon este cea mai mare rețea socială descentralizată de pe internet. În loc de un singur site, este o rețea de milioane de utilizatori din comunități independente care pot interacționa cu ceilalți, fără nici o întrerupere. Indiferent în ce te afli, poți întâlni oameni pasionați care postează despre asta pe Mastodon!
|
||||
|
||||
Alătură-te unei comunități și creează-ți profilul. Găsește și urmărește oameni fascinanți și citește postările lor într-un calendar cronologic fără reclame. Exprimă-te cu emoji-uri personalizate, imagini, GIF-uri, videoclipuri și audio în postări de 500 de caractere. Răspunde la subiectele de discuție și impulsionează postările de la oricine pentru a împărtăși lucruri minunate. Găsește conturi noi de urmărit și haștag-uri populare pentru a-ți extinde rețeaua.
|
||||
|
||||
Mastodon a fost construit cu accent pe confidențialitate și siguranță. Decide dacă postările tale sunt partajate cu urmăritorii tăi, doar cu cei pe care îi menționezi sau cu întreaga lume. Avertismentele de conținut vă permit să ascundeți postările care conțin materiale sensibile sau declanșatoare până când sunteți gata să le implicați. Fiecare comunitate are propriile sale orientări și proprii moderatori pentru a-și menține membrii în siguranță, iar instrumentele solide de blocare și raportare contribuie la prevenirea abuzurilor.
|
||||
|
||||
Mai multe caracteristici:
|
||||
|
||||
• Mod întunecat: Citește postările în modul luminos, întunecat sau negru total
|
||||
• Sondaje: Cereți celor care vă urmăresc opinia lor și numărați voturile
|
||||
• Explorează: Hașhtag-urile populare și conturile sunt la o apăsare distanță
|
||||
• Notificări: Primiți notificări despre noi urmăritori, răspunsuri și impulsionări
|
||||
• Distribuire: Postează direct pe Mastodon din orice foaie de partajare în orice aplicație
|
||||
• Drăgălășenie: Mascota noastră este un elefant adorabil, și îi veți vedea apărând din când în când
|
||||
|
||||
Mastodon este o organizație non-profit înregistrată, iar dezvoltarea este sprijinită direct de donațiile tale. Nu există publicitate, monetizare și capital de risc, și intenționăm să păstrăm lucrurile astfel.
|
||||
1
fastlane/metadata/android/ro-RO/short_description.txt
Normal file
1
fastlane/metadata/android/ro-RO/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Rețea socială descentralizată
|
||||
1
fastlane/metadata/android/ro-RO/title.txt
Normal file
1
fastlane/metadata/android/ro-RO/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
16
fastlane/metadata/android/sl-SI/full_description.txt
Normal file
16
fastlane/metadata/android/sl-SI/full_description.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Mastodon je največje decentralizirano družbeno omrežje na internetu. Namesto enega samega spletišča ga tvorijo milijoni uporabnikov v neodvisnih skupnostih, ki lahko med seboj komunicirajo brez težav. Ne glede na to, kaj vas zanima, lahko srečate predane ljudi, ki o tem objavljajo na Mastodonu!
|
||||
|
||||
Pridružite se skupnosti in ustvarite svoj profil. Poiščite in sledite zanimivim osebam ter berite njihove objave na časovnici brez oglasov v kronološkem zaporedju. Izrazite se s čustvenčki po meri, slikami, GIF-i, videoposnetki in zvočnimi posnetki v objavah z največ 500 znaki. Odgovarjajte na niti in poobjavite objave drugih, da delite dobro z drugimi. Poiščite nove račune za sledenje ter ključnike v trendu, da razširite svoje omrežje.
|
||||
|
||||
Mastodon je izdelan s poudarkom na zasebnosti in varnosti. Odločite se, ali se vaše objave delijo z vašimi sledilci, zgolj z omenjenimi ali s celim svetom. Opozorila o vsebini omogočajo skrivanje objav, ki vsebujejo občutljive ali netilne zadeve, vse dokler niste pripravljeni, da se z njimi spopadete. Vsak skupnost ima svoja lastna pravila in moderatorje, ki varujejo svoje člane, ter robustna orodja za blokiranje in poročanje, ki pomagajo preprečiti žalitve in kršitve človeškega dostojanstva ter pravic.
|
||||
|
||||
Dodatne funkcionalnosti:
|
||||
|
||||
• Temni način: objave berite v svetlem, temnem ali povsem črnem načinu;
|
||||
• Ankete: vprašajte sledilce o njihovem mnenju in preštejte njihove glasove;
|
||||
• Razišči: ključniki in računi v trendu so le en tap stran;
|
||||
• Obvestila: bodite obveščeni o novih sledenjih, odgovorih in poobjavah;
|
||||
• Skupna raba: objavljajte neposredno v Mastodon s poljubne preglednice v skupni rabi;
|
||||
• Srčkano: naša maskota je ljubek slon in videli boste, kako se sem ter tja pojavi.
|
||||
|
||||
Mastodon je registrirana neprofitna organizacija, razvoj pa podpirajo neposredno vaše donacije. Je brez oglaševanja, monetizacije in brez rizičnega kapitala; nameravamo ga takšnega tudi obdržati.
|
||||
1
fastlane/metadata/android/sl-SI/short_description.txt
Normal file
1
fastlane/metadata/android/sl-SI/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Decentralizirano družbeno omrežje
|
||||
1
fastlane/metadata/android/sl-SI/title.txt
Normal file
1
fastlane/metadata/android/sl-SI/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Mastodon
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon!
|
||||
Mastodon — найбільша децентралізована соціальна мережа в інтернеті. Замість одного сайту це мережа мільйонів користувачів у незалежних спільнотах, які можуть взаємодіяти один з одним. Незалежно від того, чим ви займаєтеся, ви можете зустріти захоплених людей, які пишуть про це на Mastodon!
|
||||
|
||||
Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
|
||||
Приєднуйтесь до спільноти і створіть свій профіль. Знайдіть і підпишіться на цікавих людей і читайте пости у вільний від реклами стрічці. Виразіть себе за допомогою користувацьких емоджі, зображень, GIF, відео й аудіо з 500-символьними постами. Відповідайте на теми й робіть репости постів від будь-кого, щоб ділитися з ними гарними матеріалами. Знаходьте нові облікові записи, щоб підписатися і популярні хештеги для розширення вашої мережі.
|
||||
|
||||
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
|
||||
Mastodon будується з акцентом на конфіденційність та безпеці. Вирішіть, чи будуть ваші пости тільки для підписників, або ті люди, з яких ви згадали, чи цілий світ. Попередження щодо вмісту дозволяють приховати публікації, що містять конфіденційний або провокаційний матеріал, доки ви не будете готові до нього. Кожна спільнота має свої правила і модераторів, щоб залишити учасників в безпеці, а також надійне блокування та інструменти для скарг, щоб запобігти зловживання.
|
||||
|
||||
More features:
|
||||
Більше можливостей:
|
||||
|
||||
• Dark Mode: Read posts in light, dark, or true black mode
|
||||
• Polls: Ask followers for their opinion and tally the votes
|
||||
• Explore: Trending hashtags and accounts are a tap away
|
||||
• Notifications: Get notified about new follows, replies, and reblogs
|
||||
• Sharing: Post directly to Mastodon from any share sheet in any app
|
||||
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
|
||||
• Темна Тема: Читайте у світлій, темній, або справжній чорній темі
|
||||
• Опитування: запитуйте думку підписникіна та підраховуйте голоси
|
||||
Досліджуйте: Популярні Хештеги й Користувачі за одним дотиком
|
||||
• Сповіщення: отримуйте сповіщення про нових підписників, відповіді та репости
|
||||
Діліться: Публікуйте безпосередньо в Mastodon з будь-якого меню "поділитися" в будь-якому додатку
|
||||
• Привабливість: Нашим талісманом є чарівний слон, і ви побачите, як він з'являється час від часу
|
||||
|
||||
Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way.
|
||||
Mastodon є зареєстрованою некомерційною організацією і розробка підтримується безпосередньо вашими пожертвуваннями. Тут немає реклами, монетизації та венчурного капіталу, і плануємо так тримати.
|
||||
@@ -1 +1 @@
|
||||
Decentralized social network
|
||||
Децентралізована соціальна мережа
|
||||
@@ -6,11 +6,11 @@ plugins {
|
||||
android {
|
||||
compileSdk 33
|
||||
defaultConfig {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 45
|
||||
versionName "1.1.4+fork.45"
|
||||
versionName "1.1.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
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",
|
||||
@@ -56,9 +56,6 @@ android {
|
||||
githubRelease{
|
||||
setRoot "src/github"
|
||||
}
|
||||
debug {
|
||||
setRoot "src/github"
|
||||
}
|
||||
}
|
||||
lintOptions{
|
||||
checkReleaseBuilds false
|
||||
|
||||
7
mastodon/proguard-rules.pro
vendored
7
mastodon/proguard-rules.pro
vendored
@@ -46,9 +46,4 @@
|
||||
|
||||
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
||||
|
||||
-keepattributes LineNumberTable
|
||||
|
||||
# Parceler library
|
||||
-keep interface org.parceler.Parcel
|
||||
-keep @org.parceler.Parcel class * { *; }
|
||||
-keep class **$$Parcelable { *; }
|
||||
-keepattributes LineNumberTable
|
||||
@@ -26,8 +26,6 @@ import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -38,7 +36,7 @@ import okhttp3.Response;
|
||||
|
||||
@Keep
|
||||
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
private static final long CHECK_PERIOD=6*3600*1000L;
|
||||
private static final long CHECK_PERIOD=24*3600*1000L;
|
||||
private static final String TAG="GithubSelfUpdater";
|
||||
|
||||
private UpdateState state=UpdateState.NO_UPDATE;
|
||||
@@ -96,51 +94,38 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
||||
public void maybeCheckForUpdates(){
|
||||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||
return;
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
|
||||
if(timeSinceLastCheck>=CHECK_PERIOD){
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
||||
if(timeSinceLastCheck>CHECK_PERIOD){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkForUpdates() {
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
|
||||
private void actuallyCheckForUpdates(){
|
||||
Request req=new Request.Builder()
|
||||
.url("https://api.github.com/repos/sk22/mastodos/releases/latest")
|
||||
.url("https://api.github.com/repos/mastodon/mastodon-android/releases/latest")
|
||||
.build();
|
||||
Call call=MastodonAPIController.getHttpClient().newCall(req);
|
||||
try(Response resp=call.execute()){
|
||||
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||
String tag=obj.get("tag_name").getAsString();
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)");
|
||||
Matcher matcher=pattern.matcher(tag);
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||
return;
|
||||
}
|
||||
int newMajor=Integer.parseInt(matcher.group(1)),
|
||||
newMinor=Integer.parseInt(matcher.group(2)),
|
||||
newRevision=Integer.parseInt(matcher.group(3)),
|
||||
newForkNumber=Integer.parseInt(matcher.group(4));
|
||||
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=Integer.parseInt(matcher.group(3));
|
||||
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
||||
return;
|
||||
}
|
||||
int curMajor=Integer.parseInt(matcher.group(1)),
|
||||
curMinor=Integer.parseInt(matcher.group(2)),
|
||||
curRevision=Integer.parseInt(matcher.group(3)),
|
||||
curForkNumber=Integer.parseInt(matcher.group(4));
|
||||
int curMajor=Integer.parseInt(matcher.group(1)), curMinor=Integer.parseInt(matcher.group(2)), curRevision=Integer.parseInt(matcher.group(3));
|
||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||
if(newVersion>curVersion || newForkNumber>curForkNumber || BuildConfig.DEBUG){
|
||||
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
|
||||
if(newVersion>curVersion || BuildConfig.DEBUG){
|
||||
String version=newMajor+"."+newMinor+"."+newRevision;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
JsonObject asset=el.getAsJsonObject();
|
||||
|
||||
@@ -7,15 +7,9 @@ public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean trueBlackTheme;
|
||||
public static boolean showReplies;
|
||||
public static boolean showBoosts;
|
||||
public static boolean loadNewPosts;
|
||||
public static boolean showInteractionCounts;
|
||||
public static boolean alwaysExpandContentWarnings;
|
||||
public static boolean disableMarquee;
|
||||
public static ThemePreference theme;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@@ -24,12 +18,6 @@ public class GlobalUserPreferences{
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
||||
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
}
|
||||
|
||||
@@ -37,13 +25,7 @@ public class GlobalUserPreferences{
|
||||
getPrefs().edit()
|
||||
.putBoolean("playGifs", playGifs)
|
||||
.putBoolean("useCustomTabs", useCustomTabs)
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putBoolean("loadNewPosts", loadNewPosts)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putBoolean("showInteractionCounts", showInteractionCounts)
|
||||
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
||||
.putBoolean("disableMarquee", disableMarquee)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import me.grishka.appkit.utils.WorkerThread;
|
||||
|
||||
public class CacheController{
|
||||
private static final String TAG="CacheController";
|
||||
private static final int DB_VERSION=3;
|
||||
private static final int DB_VERSION=2;
|
||||
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
|
||||
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
|
||||
|
||||
@@ -73,7 +73,7 @@ public class CacheController{
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status.getContentStatus().content))
|
||||
if(filter.matches(status))
|
||||
continue outer;
|
||||
}
|
||||
result.add(status);
|
||||
@@ -126,15 +126,14 @@ public class CacheController{
|
||||
});
|
||||
}
|
||||
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
|
||||
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
|
||||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`id` DESC", count+"")){
|
||||
if(cursor.getCount()==count){
|
||||
ArrayList<Notification> result=new ArrayList<>();
|
||||
cursor.moveToFirst();
|
||||
@@ -146,7 +145,7 @@ public class CacheController{
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status.getContentStatus().content))
|
||||
if(filter.matches(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
@@ -160,21 +159,21 @@ public class CacheController{
|
||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
||||
}
|
||||
}
|
||||
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
||||
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Notification> result){
|
||||
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(ntf.status.getContentStatus().content)){
|
||||
if(filter.matches(ntf.status)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
|
||||
putNotifications(result, onlyMentions, onlyPosts, maxID==null);
|
||||
putNotifications(result, onlyMentions, maxID==null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -192,9 +191,9 @@ public class CacheController{
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean onlyPosts, boolean clear){
|
||||
private void putNotifications(List<Notification> notifications, boolean onlyMentions, boolean clear){
|
||||
runOnDbThread((db)->{
|
||||
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
String table=onlyMentions ? "notifications_mentions" : "notifications_all";
|
||||
if(clear)
|
||||
db.delete(table, null, null);
|
||||
ContentValues values=new ContentValues(3);
|
||||
@@ -318,7 +317,6 @@ public class CacheController{
|
||||
`type` INTEGER NOT NULL
|
||||
)""");
|
||||
createRecentSearchesTable(db);
|
||||
createPostsNotificationsTable(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -326,9 +324,6 @@ public class CacheController{
|
||||
if(oldVersion==1){
|
||||
createRecentSearchesTable(db);
|
||||
}
|
||||
if(oldVersion==2){
|
||||
createPostsNotificationsTable(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void createRecentSearchesTable(SQLiteDatabase db){
|
||||
@@ -339,16 +334,6 @@ public class CacheController{
|
||||
`time` INTEGER NOT NULL
|
||||
)""");
|
||||
}
|
||||
|
||||
private void createPostsNotificationsTable(SQLiteDatabase db){
|
||||
db.execSQL("""
|
||||
CREATE TABLE `notifications_posts` (
|
||||
`id` VARCHAR(25) NOT NULL PRIMARY KEY,
|
||||
`json` TEXT NOT NULL,
|
||||
`flags` INTEGER NOT NULL DEFAULT 0,
|
||||
`type` INTEGER NOT NULL
|
||||
)""");
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonIOException;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -26,7 +27,10 @@ public class JsonObjectRequestBody extends RequestBody{
|
||||
public void writeTo(BufferedSink sink) throws IOException{
|
||||
try{
|
||||
OutputStreamWriter writer=new OutputStreamWriter(sink.outputStream(), StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(obj, writer);
|
||||
if(obj instanceof JsonElement)
|
||||
writer.write(obj.toString());
|
||||
else
|
||||
MastodonAPIController.gson.toJson(obj, writer);
|
||||
writer.flush();
|
||||
}catch(JsonIOException x){
|
||||
throw new IOException(x);
|
||||
|
||||
@@ -365,6 +365,8 @@ public class PushSubscriptionManager{
|
||||
}
|
||||
|
||||
private static void registerAllAccountsForPush(boolean forceReRegister){
|
||||
if(!arePushNotificationsAvailable())
|
||||
return;
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
if(session.pushSubscription==null || forceReRegister)
|
||||
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);
|
||||
|
||||
@@ -63,36 +63,6 @@ public class StatusInteractionController{
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setBookmarked(Status status, boolean bookmarked){
|
||||
if(!Looper.getMainLooper().isCurrentThread())
|
||||
throw new IllegalStateException("Can only be called from main thread");
|
||||
|
||||
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
|
||||
if(current!=null){
|
||||
current.cancel();
|
||||
}
|
||||
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
error.showToast(MastodonApp.context);
|
||||
status.bookmarked=!bookmarked;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningBookmarkRequests.put(status.id, req);
|
||||
status.bookmarked=bookmarked;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setReblogged(Status status, boolean reblogged){
|
||||
if(!Looper.getMainLooper().isCurrentThread())
|
||||
throw new IllegalStateException("Can only be called from main thread");
|
||||
@@ -130,4 +100,34 @@ public class StatusInteractionController{
|
||||
status.reblogsCount--;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setBookmarked(Status status, boolean bookmarked){
|
||||
if(!Looper.getMainLooper().isCurrentThread())
|
||||
throw new IllegalStateException("Can only be called from main thread");
|
||||
|
||||
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
|
||||
if(current!=null){
|
||||
current.cancel();
|
||||
}
|
||||
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
error.showToast(MastodonApp.context);
|
||||
status.bookmarked=!bookmarked;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningBookmarkRequests.put(status.id, req);
|
||||
status.bookmarked=bookmarked;
|
||||
E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,21 @@ public class IsoInstantTypeAdapter extends TypeAdapter<Instant>{
|
||||
in.nextNull();
|
||||
return null;
|
||||
}
|
||||
try{
|
||||
return DateTimeFormatter.ISO_INSTANT.parse(in.nextString(), Instant::from);
|
||||
}catch(DateTimeParseException x){
|
||||
String nextString;
|
||||
try {
|
||||
nextString = in.nextString();
|
||||
}catch(Exception e){
|
||||
return null;
|
||||
}
|
||||
|
||||
try{
|
||||
return DateTimeFormatter.ISO_INSTANT.parse(nextString, Instant::from);
|
||||
}catch(DateTimeParseException x){}
|
||||
|
||||
try{
|
||||
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(nextString, Instant::from);
|
||||
}catch(DateTimeParseException x){}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.joinmastodon.android.api.gson;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
|
||||
public class JsonArrayBuilder{
|
||||
private JsonArray arr=new JsonArray();
|
||||
|
||||
public JsonArrayBuilder add(JsonElement el){
|
||||
arr.add(el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonArrayBuilder add(String el){
|
||||
arr.add(el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonArrayBuilder add(Number el){
|
||||
arr.add(el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonArrayBuilder add(boolean el){
|
||||
arr.add(el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonArrayBuilder add(JsonObjectBuilder el){
|
||||
arr.add(el.build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonArrayBuilder add(JsonArrayBuilder el){
|
||||
arr.add(el.build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonArray build(){
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.joinmastodon.android.api.gson;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
public class JsonObjectBuilder{
|
||||
private JsonObject obj=new JsonObject();
|
||||
|
||||
public JsonObjectBuilder add(String key, JsonElement el){
|
||||
obj.add(key, el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder add(String key, String el){
|
||||
obj.addProperty(key, el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder add(String key, Number el){
|
||||
obj.addProperty(key, el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder add(String key, boolean el){
|
||||
obj.addProperty(key, el);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder add(String key, JsonObjectBuilder el){
|
||||
obj.add(key, el.build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonObjectBuilder add(String key, JsonArrayBuilder el){
|
||||
obj.add(key, el.build());
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonObject build(){
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class AuthorizeFollowRequest extends MastodonAPIRequest<Relationship>{
|
||||
public AuthorizeFollowRequest(String id){
|
||||
super(HttpMethod.POST, "/follow_requests/"+id+"/authorize", Relationship.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
switch(filter){
|
||||
case DEFAULT -> addQueryParameter("exclude_replies", "true");
|
||||
case INCLUDE_REPLIES -> {}
|
||||
case PINNED -> addQueryParameter("pinned", "true");
|
||||
case MEDIA -> addQueryParameter("only_media", "true");
|
||||
case NO_REBLOGS -> {
|
||||
addQueryParameter("exclude_replies", "true");
|
||||
@@ -34,7 +33,6 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
public enum Filter{
|
||||
DEFAULT,
|
||||
INCLUDE_REPLIES,
|
||||
PINNED,
|
||||
MEDIA,
|
||||
NO_REBLOGS,
|
||||
OWN_POSTS_AND_REPLIES
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
|
||||
private String maxId;
|
||||
|
||||
public GetBookmarks(String maxID, String minID, int limit){
|
||||
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
|
||||
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
|
||||
String link=httpResponse.header("link");
|
||||
// parsing link header by hand; using a library would be cleaner
|
||||
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
||||
if(link==null) return;
|
||||
String maxIdEq="max_id=";
|
||||
for(String s : link.split(",")) {
|
||||
if(s.contains("rel=\"next\"")) {
|
||||
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
||||
int end=s.indexOf('>');
|
||||
if(start<0 || start>end) return;
|
||||
this.maxId=s.substring(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getMaxId() {
|
||||
return maxId;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetFavourites extends MastodonAPIRequest<List<Status>>{
|
||||
private String maxId;
|
||||
|
||||
public GetFavourites(String maxID, String minID, int limit){
|
||||
super(HttpMethod.GET, "/favourites", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
|
||||
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
|
||||
String link=httpResponse.header("link");
|
||||
// parsing link header by hand; using a library would be cleaner
|
||||
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
||||
if(link==null) return;
|
||||
String maxIdEq="max_id=";
|
||||
for(String s : link.split(",")) {
|
||||
if(s.contains("rel=\"next\"")) {
|
||||
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
||||
int end=s.indexOf('>');
|
||||
if(start<0 || start>end) return;
|
||||
this.maxId=s.substring(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getMaxId() {
|
||||
return maxId;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class GetFollowRequests extends MastodonAPIRequest<List<Account>>{
|
||||
private String maxId;
|
||||
|
||||
public GetFollowRequests(String maxID, String minID, int limit){
|
||||
super(HttpMethod.GET, "/follow_requests", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(List<Account> respObj, Response httpResponse) throws IOException {
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
// <https://mastodon.social/api/v1/follow_requests?max_id=268962>; rel="next",
|
||||
// <https://mastodon.social/api/v1/follow_requests?min_id=268981>; rel="prev"
|
||||
String link=httpResponse.header("link");
|
||||
// parsing link header by hand; using a library would be cleaner
|
||||
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
|
||||
if(link==null) return;
|
||||
String maxIdEq="max_id=";
|
||||
for(String s : link.split(",")) {
|
||||
if(s.contains("rel=\"next\"")) {
|
||||
int start=s.indexOf(maxIdEq)+maxIdEq.length();
|
||||
int end=s.indexOf('>');
|
||||
if(start<0 || start>end) return;
|
||||
this.maxId=s.substring(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getMaxId() {
|
||||
return maxId;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class RejectFollowRequest extends MastodonAPIRequest<Relationship>{
|
||||
public RejectFollowRequest(String id){
|
||||
super(HttpMethod.POST, "/follow_requests/"+id+"/reject", Relationship.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
|
||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
|
||||
if(followed)
|
||||
setRequestBody(new Request(showReblogs, notify));
|
||||
setRequestBody(new Request(showReblogs, null));
|
||||
else
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import java.util.List;
|
||||
|
||||
public class AddAccountsToList extends MastodonAPIRequest<Object> {
|
||||
public AddAccountsToList(String listId, List<String> accountIds){
|
||||
super(HttpMethod.POST, "/lists/"+listId+"/accounts", Object.class);
|
||||
Request req = new Request();
|
||||
req.accountIds = accountIds;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
public static class Request{
|
||||
public List<String> accountIds;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
|
||||
public GetLists() {
|
||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||
}
|
||||
public GetLists(String accountID) {
|
||||
super(HttpMethod.GET, "/accounts/"+accountID+"/lists", new TypeToken<>(){});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import java.util.List;
|
||||
|
||||
public class RemoveAccountsFromList extends MastodonAPIRequest<Object> {
|
||||
public RemoveAccountsFromList(String listId, List<String> accountIds){
|
||||
super(HttpMethod.DELETE, "/lists/"+listId+"/accounts", Object.class);
|
||||
Request req = new Request();
|
||||
req.accountIds = accountIds;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
public static class Request{
|
||||
public List<String> accountIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.joinmastodon.android.api.requests.markers;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||
import org.joinmastodon.android.model.Marker;
|
||||
|
||||
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
|
||||
super(HttpMethod.POST, "/markers", Response.class);
|
||||
JsonObjectBuilder builder=new JsonObjectBuilder();
|
||||
if(lastSeenHomePostID!=null)
|
||||
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
|
||||
if(lastSeenNotificationID!=null)
|
||||
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Marker home, notifications;
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String clientName="Mastodos";
|
||||
public String clientName="Mastodon for Android";
|
||||
public String redirectUris=AccountSessionManager.REDIRECT_URI;
|
||||
public String scopes=AccountSessionManager.SCOPE;
|
||||
public String website="https://sk22.github.io/mastodos";
|
||||
public String website="https://app.joinmastodon.org/android";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class GetBookmarkedStatuses extends HeaderPaginationRequest<Status>{
|
||||
public GetBookmarkedStatuses(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class GetFavoritedStatuses extends HeaderPaginationRequest<Status>{
|
||||
public GetFavoritedStatuses(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/favourites", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class SetStatusPinned extends MastodonAPIRequest<Status>{
|
||||
public SetStatusPinned(String id, boolean pinned){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class GetHashtag extends MastodonAPIRequest<Hashtag> {
|
||||
public GetHashtag(String name){
|
||||
super(HttpMethod.GET, "/tags/"+name, Hashtag.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class SetHashtagFollowed extends MastodonAPIRequest<Hashtag>{
|
||||
public SetHashtagFollowed(String name, boolean followed){
|
||||
super(HttpMethod.POST, "/tags/"+name+"/"+(followed ? "follow" : "unfollow"), Hashtag.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID) {
|
||||
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class FollowRequestHandledEvent {
|
||||
public String accountID;
|
||||
public boolean accepted;
|
||||
public Account account;
|
||||
public Relationship relationship;
|
||||
|
||||
public FollowRequestHandledEvent(String accountID, boolean accepted, Account account, Relationship rel){
|
||||
this.accountID=accountID;
|
||||
this.accepted=accepted;
|
||||
this.account=account;
|
||||
this.relationship=rel;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class NotificationDeletedEvent{
|
||||
public final String id;
|
||||
|
||||
public NotificationDeletedEvent(String id){
|
||||
this.id=id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class RemoveAccountPostsEvent{
|
||||
public final String accountID;
|
||||
public final String postsByAccountID;
|
||||
public final boolean isUnfollow;
|
||||
|
||||
public RemoveAccountPostsEvent(String accountID, String postsByAccountID, boolean isUnfollow){
|
||||
this.accountID=accountID;
|
||||
this.postsByAccountID=postsByAccountID;
|
||||
this.isUnfollow=isUnfollow;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import org.joinmastodon.android.model.Status;
|
||||
public class StatusCountersUpdatedEvent{
|
||||
public String id;
|
||||
public long favorites, reblogs, replies;
|
||||
public boolean favorited, reblogged, pinned;
|
||||
public boolean favorited, reblogged, bookmarked;
|
||||
|
||||
public StatusCountersUpdatedEvent(Status s){
|
||||
id=s.id;
|
||||
@@ -14,6 +14,6 @@ public class StatusCountersUpdatedEvent{
|
||||
replies=s.repliesCount;
|
||||
favorited=s.favourited;
|
||||
reblogged=s.reblogged;
|
||||
pinned=s.pinned;
|
||||
bookmarked=s.bookmarked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package org.joinmastodon.android.events;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusCreatedEvent{
|
||||
public Status status;
|
||||
public final Status status;
|
||||
public final String accountID;
|
||||
|
||||
public StatusCreatedEvent(Status status){
|
||||
public StatusCreatedEvent(Status status, String accountID){
|
||||
this.status=status;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class StatusUnpinnedEvent {
|
||||
public final String id;
|
||||
public final String accountID;
|
||||
|
||||
public StatusUnpinnedEvent(String id, String accountID){
|
||||
this.id=id;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,10 @@ import android.view.View;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -78,7 +77,6 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
protected void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
|
||||
return;
|
||||
if(filter==GetAccountStatuses.Filter.PINNED) return;
|
||||
if(filter==GetAccountStatuses.Filter.DEFAULT){
|
||||
// Keep replies to self, discard all other replies
|
||||
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
|
||||
@@ -90,23 +88,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
}
|
||||
|
||||
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
|
||||
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
|
||||
return;
|
||||
|
||||
Status status=getStatusByID(ev.id);
|
||||
data.remove(status);
|
||||
preloadedData.remove(status);
|
||||
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
|
||||
if(item==null)
|
||||
return;
|
||||
int index=displayItems.indexOf(item);
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
@Override
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import android.view.WindowInsets;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||
@@ -82,10 +81,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(GlobalUserPreferences.disableMarquee){
|
||||
setTitleMarqueeEnabled(false);
|
||||
setSubtitleMarqueeEnabled(false);
|
||||
}
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
}
|
||||
@@ -444,7 +439,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
private String nextMaxID;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.bookmarks);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetBookmarkedStatuses(offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class BookmarksListFragment extends StatusListFragment{
|
||||
|
||||
private String accountID;
|
||||
private Account self;
|
||||
private String lastMaxId=null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
self=session.self;
|
||||
setTitle(R.string.bookmarks);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count) {
|
||||
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
|
||||
currentRequest=b.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, b.getMaxId()!=null);
|
||||
lastMaxId=b.getMaxId();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import android.graphics.Outline;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.RenderEffect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.icu.text.BreakIterator;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
@@ -28,7 +27,6 @@ import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -52,7 +50,6 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.twitter.twittertext.Regex;
|
||||
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -102,7 +99,6 @@ import org.parceler.Parcels;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -133,21 +129,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
|
||||
private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
|
||||
|
||||
private static final String VALID_URL_PATTERN_STRING =
|
||||
"(" + // $1 total match
|
||||
"(" + Regex.URL_VALID_PRECEDING_CHARS + ")" + // $2 Preceding character
|
||||
"(" + // $3 URL
|
||||
"(https?://)" + // $4 Protocol (optional)
|
||||
"(" + Regex.URL_VALID_DOMAIN + ")" + // $5 Domain(s)
|
||||
"(?::(" + Regex.URL_VALID_PORT_NUMBER + "))?" + // $6 Port number (optional)
|
||||
"(/" +
|
||||
Regex.URL_VALID_PATH + "*+" +
|
||||
")?" + // $7 URL Path and anchor
|
||||
"(\\?" + Regex.URL_VALID_URL_QUERY_CHARS + "*" + // $8 Query String
|
||||
Regex.URL_VALID_URL_QUERY_ENDING_CHARS + ")?" +
|
||||
")" +
|
||||
")";
|
||||
private static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
|
||||
@SuppressLint("NewApi") // this class actually exists on 6.0
|
||||
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
|
||||
|
||||
@@ -164,13 +145,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private Button publishButton;
|
||||
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
|
||||
private ImageView sensitiveIcon;
|
||||
private ComposeMediaLayout attachmentsView;
|
||||
private TextView replyText;
|
||||
private ReorderableLinearLayout pollOptionsView;
|
||||
private View pollWrap;
|
||||
private View addPollOptionBtn;
|
||||
private View sensitiveItem;
|
||||
private TextView pollDurationView;
|
||||
|
||||
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
||||
@@ -186,7 +165,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private String pollDurationStr;
|
||||
private EditText spoilerEdit;
|
||||
private boolean hasSpoiler;
|
||||
private boolean sensitive;
|
||||
private ProgressBar sendProgress;
|
||||
private ImageView sendError;
|
||||
private View sendingOverlay;
|
||||
@@ -199,7 +177,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private boolean attachmentsErrorShowing;
|
||||
|
||||
private Status editingStatus;
|
||||
private boolean redraftStatus;
|
||||
private boolean pollChanged;
|
||||
private boolean creatingView;
|
||||
private boolean ignoreSelectionChanges=false;
|
||||
@@ -218,7 +195,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
|
||||
if(getArguments().containsKey("editStatus")){
|
||||
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
|
||||
redraftStatus=getArguments().getBoolean("redraftStatus");
|
||||
}
|
||||
if(instance==null){
|
||||
Nav.finish(this);
|
||||
@@ -290,8 +266,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);
|
||||
sensitiveIcon=view.findViewById(R.id.sensitive_icon);
|
||||
sensitiveItem=view.findViewById(R.id.sensitive_item);
|
||||
replyText=view.findViewById(R.id.reply_text);
|
||||
|
||||
mediaBtn.setOnClickListener(v->openFilePicker());
|
||||
@@ -299,7 +273,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
||||
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
||||
visibilityBtn.setOnClickListener(this::onVisibilityClick);
|
||||
sensitiveItem.setOnClickListener(v->toggleSensitive());
|
||||
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
|
||||
@Override
|
||||
public void onIconChanged(int icon){
|
||||
@@ -384,7 +357,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(editingStatus!=null && editingStatus.visibility!=null) {
|
||||
statusVisibility=editingStatus.visibility;
|
||||
}
|
||||
|
||||
updateVisibilityIcon();
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
@@ -410,7 +382,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
outState.putInt("pollDuration", pollDuration);
|
||||
outState.putString("pollDurationStr", pollDurationStr);
|
||||
}
|
||||
outState.putBoolean("sensitive", sensitive);
|
||||
outState.putBoolean("hasSpoiler", hasSpoiler);
|
||||
if(!attachments.isEmpty()){
|
||||
ArrayList<Parcelable> serializedAttachments=new ArrayList<>(attachments.size());
|
||||
@@ -503,24 +474,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
|
||||
if(replyTo!=null){
|
||||
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;
|
||||
});
|
||||
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);
|
||||
|
||||
ArrayList<String> mentions=new ArrayList<>();
|
||||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||
if(!replyTo.account.id.equals(ownID))
|
||||
@@ -538,15 +491,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
ignoreSelectionChanges=true;
|
||||
mainEditText.setSelection(mainEditText.length());
|
||||
ignoreSelectionChanges=false;
|
||||
if(!TextUtils.isEmpty(replyTo.spoilerText)){
|
||||
if(!TextUtils.isEmpty(replyTo.spoilerText) && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)){
|
||||
hasSpoiler=true;
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
spoilerEdit.setText(replyTo.spoilerText);
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
}
|
||||
}else if (editingStatus==null || editingStatus.inReplyToId==null){
|
||||
// TODO: remove workaround after https://github.com/mastodon/mastodon-android/issues/341 gets fixed
|
||||
}else{
|
||||
replyText.setVisibility(View.GONE);
|
||||
}
|
||||
if(savedInstanceState==null){
|
||||
@@ -587,18 +539,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
|
||||
updateSensitive();
|
||||
|
||||
if(editingStatus!=null){
|
||||
updateCharCounter();
|
||||
visibilityBtn.setEnabled(redraftStatus);
|
||||
visibilityBtn.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
publishButton=new Button(getActivity());
|
||||
publishButton.setText(editingStatus==null || redraftStatus ? R.string.publish : R.string.save);
|
||||
publishButton.setText(editingStatus==null ? R.string.publish : R.string.save);
|
||||
publishButton.setOnClickListener(this::onPublishClick);
|
||||
LinearLayout wrap=new LinearLayout(getActivity());
|
||||
wrap.setOrientation(LinearLayout.HORIZONTAL);
|
||||
@@ -644,7 +594,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
|
||||
MENTION_PATTERN.matcher(
|
||||
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
||||
HtmlParser.URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
||||
).replaceAll("$1@$3")
|
||||
).replaceAll("x");
|
||||
charCount=0;
|
||||
@@ -699,12 +649,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
req.status=text;
|
||||
req.visibility=statusVisibility;
|
||||
req.sensitive=sensitive;
|
||||
if(!attachments.isEmpty()){
|
||||
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
||||
}
|
||||
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
|
||||
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
|
||||
if(replyTo!=null){
|
||||
req.inReplyToId=replyTo.id;
|
||||
}
|
||||
if(!pollOptions.isEmpty()){
|
||||
req.poll=new CreateStatus.Request.Poll();
|
||||
@@ -738,7 +687,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
if(editingStatus==null){
|
||||
E.post(new StatusCreatedEvent(result));
|
||||
E.post(new StatusCreatedEvent(result, accountID));
|
||||
if(replyTo!=null){
|
||||
replyTo.repliesCount++;
|
||||
E.post(new StatusCountersUpdatedEvent(replyTo));
|
||||
@@ -747,13 +696,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
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
|
||||
@@ -767,7 +709,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
};
|
||||
|
||||
if(editingStatus!=null && !redraftStatus){
|
||||
if(editingStatus!=null){
|
||||
new EditStatus(req, editingStatus.id)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
@@ -779,7 +721,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private boolean hasDraft(){
|
||||
if(getArguments().getBoolean("hasDraft", false)) return true;
|
||||
if(editingStatus!=null){
|
||||
if(!mainEditText.getText().toString().equals(initialText))
|
||||
return true;
|
||||
@@ -913,7 +854,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
uploadNextQueuedAttachment();
|
||||
}
|
||||
updatePublishButtonState();
|
||||
updateSensitive();
|
||||
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS)
|
||||
mediaBtn.setEnabled(false);
|
||||
return true;
|
||||
@@ -1088,7 +1028,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
updatePublishButtonState();
|
||||
pollBtn.setEnabled(attachments.isEmpty());
|
||||
mediaBtn.setEnabled(true);
|
||||
updateSensitive();
|
||||
}
|
||||
|
||||
private void onRetryOrCancelMediaUploadClick(View v){
|
||||
@@ -1290,20 +1229,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
spoilerBtn.setSelected(false);
|
||||
mainEditText.requestFocus();
|
||||
updateCharCounter();
|
||||
sensitiveIcon.setVisibility(getMediaAttachmentsCount() > 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
updateSensitive();
|
||||
}
|
||||
|
||||
private void toggleSensitive() {
|
||||
sensitive=!sensitive;
|
||||
sensitiveIcon.setSelected(sensitive);
|
||||
}
|
||||
|
||||
private void updateSensitive() {
|
||||
sensitiveItem.setVisibility(View.GONE);
|
||||
if (!attachments.isEmpty() && !hasSpoiler) sensitiveItem.setVisibility(View.VISIBLE);
|
||||
if (attachments.isEmpty()) sensitive = false;
|
||||
}
|
||||
|
||||
private int getMediaAttachmentsCount(){
|
||||
@@ -1317,8 +1243,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), menu);
|
||||
m.setGroupCheckable(0, true, true);
|
||||
m.findItem(switch(statusVisibility){
|
||||
case PUBLIC -> R.id.vis_public;
|
||||
case UNLISTED -> R.id.vis_unlisted;
|
||||
case PUBLIC, UNLISTED -> R.id.vis_public;
|
||||
case PRIVATE -> R.id.vis_followers;
|
||||
case DIRECT -> R.id.vis_private;
|
||||
}).setChecked(true);
|
||||
@@ -1328,8 +1253,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.vis_public){
|
||||
statusVisibility=StatusPrivacy.PUBLIC;
|
||||
}else if(id==R.id.vis_unlisted){
|
||||
statusVisibility=StatusPrivacy.UNLISTED;
|
||||
}else if(id==R.id.vis_followers){
|
||||
statusVisibility=StatusPrivacy.PRIVATE;
|
||||
}else if(id==R.id.vis_private){
|
||||
@@ -1360,9 +1283,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onSuccess(Preferences result){
|
||||
// Only override the reply visibility if our preference is more private
|
||||
if (result.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
|
||||
// Map unlisted from the API onto public, because we don't have unlisted in the UI
|
||||
statusVisibility = switch (result.postingDefaultVisibility) {
|
||||
case PUBLIC -> StatusPrivacy.PUBLIC;
|
||||
case UNLISTED -> StatusPrivacy.UNLISTED;
|
||||
case PUBLIC, UNLISTED -> StatusPrivacy.PUBLIC;
|
||||
case PRIVATE -> StatusPrivacy.PRIVATE;
|
||||
case DIRECT -> StatusPrivacy.DIRECT;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FavoritedStatusListFragment extends StatusListFragment{
|
||||
private String nextMaxID;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.your_favorites);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetFavoritedStatuses(offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFavourites;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FavoritesListFragment extends StatusListFragment{
|
||||
|
||||
private String accountID;
|
||||
private String lastMaxId=null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
setTitle(R.string.favorited_posts);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count) {
|
||||
GetFavourites b=new GetFavourites(offset>0 ? lastMaxId : null, null, count);
|
||||
currentRequest=b.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, b.getMaxId()!=null);
|
||||
lastMaxId=b.getMaxId();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
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.ProgressBarButton;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
private String lastMaxId=null;
|
||||
|
||||
public FollowRequestsListFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.follow_requests);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(relationshipsRequest!=null){
|
||||
relationshipsRequest.cancel();
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
currentRequest=new GetFollowRequests(offset>0 ? lastMaxId : null, null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Account> result){
|
||||
onDataLoaded(result.stream().map(AccountWrapper::new).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new AccountsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
outRect.bottom=outRect.left=outRect.right=V.dp(16);
|
||||
if(parent.getChildAdapterPosition(view)==0)
|
||||
outRect.top=V.dp(16);
|
||||
}
|
||||
});
|
||||
((UsableRecyclerView)list).setDrawSelectorOnTop(true);
|
||||
}
|
||||
|
||||
private void loadRelationships(){
|
||||
relationships=Collections.emptyMap();
|
||||
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
|
||||
relationshipsRequest.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> result){
|
||||
relationshipsRequest=null;
|
||||
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof AccountViewHolder avh)
|
||||
avh.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(relationshipsRequest!=null){
|
||||
relationshipsRequest.cancel();
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new AccountViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return 2+data.get(position).emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
AccountWrapper item=data.get(position);
|
||||
if(image==0)
|
||||
return item.avaRequest;
|
||||
else if(image==1)
|
||||
return item.coverRequest;
|
||||
else
|
||||
return item.emojiHelper.getImageRequest(image-2);
|
||||
}
|
||||
}
|
||||
|
||||
// literally the same as AccountCardStatusDisplayItem and DiscoverAccountsFragment. code should be generalized
|
||||
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final ImageView cover, avatar;
|
||||
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
|
||||
private final ProgressBarButton actionButton, acceptButton, rejectButton;
|
||||
private final ProgressBar actionProgress, acceptProgress, rejectProgress;
|
||||
private final View actionWrap, acceptWrap, rejectWrap;
|
||||
|
||||
private Relationship relationship;
|
||||
|
||||
public AccountViewHolder(){
|
||||
super(getActivity(), R.layout.item_discover_account, list);
|
||||
cover=findViewById(R.id.cover);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
bio=findViewById(R.id.bio);
|
||||
followersCount=findViewById(R.id.followers_count);
|
||||
followersLabel=findViewById(R.id.followers_label);
|
||||
followingCount=findViewById(R.id.following_count);
|
||||
followingLabel=findViewById(R.id.following_label);
|
||||
postsCount=findViewById(R.id.posts_count);
|
||||
postsLabel=findViewById(R.id.posts_label);
|
||||
actionButton=findViewById(R.id.action_btn);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
actionWrap=findViewById(R.id.action_btn_wrap);
|
||||
acceptButton=findViewById(R.id.accept_btn);
|
||||
acceptProgress=findViewById(R.id.accept_progress);
|
||||
acceptWrap=findViewById(R.id.accept_btn_wrap);
|
||||
rejectButton=findViewById(R.id.reject_btn);
|
||||
rejectProgress=findViewById(R.id.reject_progress);
|
||||
rejectWrap=findViewById(R.id.reject_btn_wrap);
|
||||
|
||||
itemView.setOutlineProvider(OutlineProviders.roundedRect(6));
|
||||
itemView.setClipToOutline(true);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setClipToOutline(true);
|
||||
cover.setOutlineProvider(OutlineProviders.roundedRect(3));
|
||||
cover.setClipToOutline(true);
|
||||
actionButton.setOnClickListener(this::onActionButtonClick);
|
||||
acceptButton.setOnClickListener(this::onFollowRequestButtonClick);
|
||||
rejectButton.setOnClickListener(this::onFollowRequestButtonClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountWrapper item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.account.acct);
|
||||
bio.setText(item.parsedBio);
|
||||
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
|
||||
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
|
||||
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||
relationship=relationships.get(item.account.id);
|
||||
if(relationship == null || !relationship.followedBy){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
acceptWrap.setVisibility(View.VISIBLE);
|
||||
rejectWrap.setVisibility(View.VISIBLE);
|
||||
|
||||
// i hate that i wasn't able to do this in xml
|
||||
acceptButton.setCompoundDrawableTintList(acceptButton.getTextColors());
|
||||
acceptProgress.setIndeterminateTintList(acceptButton.getTextColors());
|
||||
rejectButton.setCompoundDrawableTintList(rejectButton.getTextColors());
|
||||
rejectProgress.setIndeterminateTintList(rejectButton.getTextColors());
|
||||
}else if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
acceptWrap.setVisibility(View.GONE);
|
||||
rejectWrap.setVisibility(View.GONE);
|
||||
}else{
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
acceptWrap.setVisibility(View.GONE);
|
||||
rejectWrap.setVisibility(View.GONE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else if(index==1){
|
||||
cover.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-2, image);
|
||||
name.invalidate();
|
||||
bio.invalidate();
|
||||
}
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private void onFollowRequestButtonClick(View v) {
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> {
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
|
||||
if (!rel.requested && !rel.followedBy && adapter != null) {
|
||||
data.remove(item);
|
||||
adapter.notifyItemRemoved(getBindingAdapterPosition());
|
||||
} else {
|
||||
rebind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
rebind();
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
}
|
||||
|
||||
protected class AccountWrapper{
|
||||
public Account account;
|
||||
public ImageLoaderRequest avaRequest, coverRequest;
|
||||
public CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
public CharSequence parsedName, parsedBio;
|
||||
|
||||
public AccountWrapper(Account account){
|
||||
this.account=account;
|
||||
if(!TextUtils.isEmpty(account.avatar))
|
||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
||||
if(!TextUtils.isEmpty(account.header))
|
||||
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
if(account.emojis.isEmpty()){
|
||||
parsedName=account.displayName;
|
||||
}else{
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,23 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
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.ImageButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetHashtag;
|
||||
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 java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HashtagTimelineFragment extends StatusListFragment{
|
||||
private String hashtag;
|
||||
private boolean following;
|
||||
private ImageButton fab;
|
||||
private MenuItem followButton;
|
||||
|
||||
public HashtagTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@@ -38,61 +27,10 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
updateTitle(getArguments().getString("hashtag"));
|
||||
following=getArguments().getBoolean("following", false);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
private void updateTitle(String hashtagName) {
|
||||
hashtag = hashtagName;
|
||||
hashtag=getArguments().getString("hashtag");
|
||||
setTitle('#'+hashtag);
|
||||
}
|
||||
|
||||
private void updateFollowingState(boolean newFollowing) {
|
||||
this.following = newFollowing;
|
||||
followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag));
|
||||
followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.hashtag_timeline, menu);
|
||||
followButton = menu.findItem(R.id.follow_hashtag);
|
||||
updateFollowingState(following);
|
||||
|
||||
followButton.setOnMenuItemClickListener(i -> {
|
||||
updateFollowingState(!following);
|
||||
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag i) {
|
||||
if (i.following == following) Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show();
|
||||
updateFollowingState(i.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
updateFollowingState(!following);
|
||||
}
|
||||
}).exec(accountID);
|
||||
return true;
|
||||
});
|
||||
|
||||
new GetHashtag(hashtag).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag hashtag) {
|
||||
updateTitle(hashtag.name);
|
||||
updateFollowingState(hashtag.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count)
|
||||
|
||||
@@ -241,14 +241,9 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(currentTab==R.id.tab_profile)
|
||||
if (profileFragment.onBackPressed()) return true;
|
||||
return profileFragment.onBackPressed();
|
||||
if(currentTab==R.id.tab_search)
|
||||
if (searchFragment.onBackPressed()) return true;
|
||||
if (currentTab!=R.id.tab_home) {
|
||||
tabBar.selectTab(R.id.tab_home);
|
||||
onTabSelected(R.id.tab_home);
|
||||
return true;
|
||||
}
|
||||
return searchFragment.onBackPressed();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.widget.Toolbar;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -74,13 +73,6 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
loadData();
|
||||
}
|
||||
|
||||
private List<Status> filterPosts(List<Status> items) {
|
||||
return items.stream().filter(i ->
|
||||
(GlobalUserPreferences.showReplies || i.inReplyToId == null) &&
|
||||
(GlobalUserPreferences.showBoosts || i.reblog == null)
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
@@ -90,8 +82,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
List<Status> filteredItems = filterPosts(result.items);
|
||||
onDataLoaded(filteredItems, !result.items.isEmpty());
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
@@ -162,7 +153,6 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private void loadNewPosts(){
|
||||
if (!GlobalUserPreferences.loadNewPosts) return;
|
||||
dataLoading=true;
|
||||
// The idea here is that we request the timeline such that if there are fewer than `limit` posts,
|
||||
// we'll get the currently topmost post as last in the response. This way we know there's no gap
|
||||
@@ -174,7 +164,6 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
result = filterPosts(result);
|
||||
if(result.isEmpty() || getActivity()==null)
|
||||
return;
|
||||
Status last=result.get(result.size()-1);
|
||||
@@ -268,7 +257,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(s.getContentStatus().content)){
|
||||
if(filter.matches(s)){
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
@@ -433,4 +422,9 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateState(ev.state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
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;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
|
||||
public class ListTimelineFragment extends StatusListFragment {
|
||||
private String listID;
|
||||
private String listTitle;
|
||||
private ImageButton fab;
|
||||
|
||||
public ListTimelineFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
listID=getArguments().getString("listID");
|
||||
listTitle=getArguments().getString("listTitle");
|
||||
setTitle(listTitle);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
// TODO: implement edit, delete
|
||||
// inflater.inflate(R.menu.list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count) {
|
||||
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result) {
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown() {
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetFabBottomInset(int inset) {
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
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;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
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 {
|
||||
private String accountId;
|
||||
private String profileAccountId;
|
||||
private String profileDisplayUsername;
|
||||
private HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
private HashMap<String, Boolean> userInList = new HashMap<>();
|
||||
private int inProgress = 0;
|
||||
|
||||
public ListTimelinesFragment() {
|
||||
super(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle args=getArguments();
|
||||
accountId=args.getString("account");
|
||||
|
||||
if(args.containsKey("profileAccount")){
|
||||
profileAccountId=args.getString("profileAccount");
|
||||
profileDisplayUsername=args.getString("profileDisplayUsername");
|
||||
setTitle(getString(R.string.lists_with_user, profileDisplayUsername));
|
||||
// setHasOptionsMenu(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
// Button saveButton=new Button(getActivity());
|
||||
// saveButton.setText(R.string.save);
|
||||
// saveButton.setOnClickListener(this::onSaveClick);
|
||||
// LinearLayout wrap=new LinearLayout(getActivity());
|
||||
// wrap.setOrientation(LinearLayout.HORIZONTAL);
|
||||
// wrap.addView(saveButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
// wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
||||
// wrap.setClipToPadding(false);
|
||||
// MenuItem item=menu.add(R.string.save);
|
||||
// item.setActionView(wrap);
|
||||
// item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
// }
|
||||
|
||||
private void saveListMembership(String listId, boolean isMember) {
|
||||
userInList.put(listId, isMember);
|
||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||
req.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(Object o) {}
|
||||
}).exec(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
userInListBefore.clear();
|
||||
userInList.clear();
|
||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
if (profileAccountId == null) return;
|
||||
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
if (!userInListBefore.containsKey(l.id)) {
|
||||
userInListBefore.put(l.id, false);
|
||||
}
|
||||
}
|
||||
userInList.putAll(userInListBefore);
|
||||
onDataLoaded(newLists, false);
|
||||
}
|
||||
}).exec(accountId);
|
||||
}
|
||||
})
|
||||
.exec(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter() {
|
||||
return new ListsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ListViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final CheckBox listToggle;
|
||||
|
||||
public ListViewHolder(){
|
||||
super(getActivity(), R.layout.item_list_timeline, list);
|
||||
title=findViewById(R.id.title);
|
||||
listToggle=findViewById(R.id.list_toggle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
|
||||
listToggle.setOnClickListener(this::onClickToggle);
|
||||
} else {
|
||||
listToggle.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickToggle(View view) {
|
||||
saveListMembership(item.id, listToggle.isChecked());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
UiUtils.openListTimeline(getActivity(), accountId, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,16 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
|
||||
import org.joinmastodon.android.events.FollowRequestHandledEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
@@ -26,15 +20,8 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{
|
||||
@@ -44,7 +31,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment;
|
||||
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
|
||||
|
||||
private String accountID;
|
||||
|
||||
@@ -55,36 +42,14 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
setRetainInstance(true);
|
||||
|
||||
accountID=getArguments().getString("account");
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() != R.id.follow_requests) return false;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false);
|
||||
@@ -92,13 +57,12 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[3];
|
||||
tabViews=new FrameLayout[2];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.notifications_all;
|
||||
case 1 -> R.id.notifications_mentions;
|
||||
case 2 -> R.id.notifications_posts;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -137,15 +101,9 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
mentionsFragment=new NotificationsListFragment();
|
||||
mentionsFragment.setArguments(args);
|
||||
|
||||
args=new Bundle(args);
|
||||
args.putBoolean("onlyPosts", true);
|
||||
postsFragment=new NotificationsListFragment();
|
||||
postsFragment.setArguments(args);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.notifications_all, allNotificationsFragment)
|
||||
.add(R.id.notifications_mentions, mentionsFragment)
|
||||
.add(R.id.notifications_posts, postsFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@@ -155,7 +113,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.all_notifications;
|
||||
case 1 -> R.string.mentions;
|
||||
case 2 -> R.string.posts;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
@@ -166,30 +123,12 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
return view;
|
||||
}
|
||||
|
||||
public void refreshFollowRequestsBadge() {
|
||||
new GetFollowRequests(null, null, 1).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Account> accounts) {
|
||||
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse errorResponse) {}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFollowRequestHandled(FollowRequestHandledEvent ev) {
|
||||
refreshFollowRequestsBadge();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
getFragmentForPage(pager.getCurrentItem()).scrollToTop();
|
||||
}
|
||||
|
||||
public void loadData(){
|
||||
refreshFollowRequestsBadge();
|
||||
if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading)
|
||||
allNotificationsFragment.loadData();
|
||||
}
|
||||
@@ -204,7 +143,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
return switch(page){
|
||||
case 0 -> allNotificationsFragment;
|
||||
case 1 -> mentionsFragment;
|
||||
case 2 -> postsFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
@@ -225,7 +163,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return 3;
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,9 +8,10 @@ import com.squareup.otto.Subscribe;
|
||||
|
||||
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;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -27,6 +28,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
@@ -35,7 +37,6 @@ import me.grishka.appkit.utils.V;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
private boolean onlyMentions;
|
||||
private boolean onlyPosts;
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
@@ -54,15 +55,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||
onlyPosts=getArguments().getBoolean("onlyPosts", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
super.onRefresh();
|
||||
if (getParentFragment() instanceof NotificationsFragment notificationsFragment) {
|
||||
notificationsFragment.refreshFollowRequestsBadge();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,7 +81,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else if(titleItem!=null){
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account, n);
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
|
||||
return Arrays.asList(titleItem, card);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
@@ -108,7 +100,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
if(getActivity()==null)
|
||||
@@ -122,6 +114,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
.collect(Collectors.toSet());
|
||||
loadRelationships(needRelationships);
|
||||
maxID=result.maxID;
|
||||
|
||||
if(offset==0 && !result.items.isEmpty()){
|
||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -192,14 +188,23 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onNotificationDeleted(NotificationDeletedEvent ev) {
|
||||
Notification notification = getNotificationByID(ev.id);
|
||||
if(notification==null)
|
||||
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
if(!ev.accountID.equals(accountID) || ev.isUnfollow)
|
||||
return;
|
||||
data.remove(notification);
|
||||
List<Notification> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
||||
.filter(n->n.account!=null && n.account.id.equals(ev.postsByAccountID))
|
||||
.collect(Collectors.toList());
|
||||
for(Notification n:toRemove){
|
||||
removeNotification(n);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeNotification(Notification n){
|
||||
data.remove(n);
|
||||
preloadedData.remove(n);
|
||||
int index=-1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
if(ev.id.equals(displayItems.get(i).parentID)){
|
||||
if(n.id.equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
@@ -208,11 +213,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
||||
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
}
|
||||
|
||||
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
|
||||
private ShapeDrawable background=new ShapeDrawable();
|
||||
protected ShapeDrawable background=new ShapeDrawable();
|
||||
|
||||
public BaseViewHolder(int layout){
|
||||
super(getActivity(), layout, list);
|
||||
@@ -220,6 +220,20 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
|
||||
super.onBind(item);
|
||||
title.setText(item.parsedName);
|
||||
value.setText(item.parsedValue);
|
||||
if(item.verifiedAt!=null){
|
||||
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
|
||||
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
|
||||
value.setTextColor(textColor);
|
||||
value.setLinkTextColor(textColor);
|
||||
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
|
||||
check.setTint(textColor);
|
||||
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
|
||||
}else{
|
||||
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
|
||||
value.setCompoundDrawables(null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
@@ -34,6 +36,7 @@ import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
@@ -98,10 +101,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private CoverImageView cover;
|
||||
private View avatarBorder;
|
||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
|
||||
private ProgressBarButton actionButton, notifyButton;
|
||||
private ProgressBarButton actionButton;
|
||||
private ViewPager2 pager;
|
||||
private NestedRecyclerScrollView scrollView;
|
||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
|
||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
|
||||
private ProfileAboutFragment aboutFragment;
|
||||
private TabLayout tabbar;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
@@ -109,7 +112,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private float titleTransY;
|
||||
private View postsBtn, followersBtn, followingBtn;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ProgressBar actionProgress, notifyProgress;
|
||||
private ProgressBar actionProgress;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private TextView followsYouView;
|
||||
@@ -181,7 +184,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
postsLabel=content.findViewById(R.id.posts_label);
|
||||
postsBtn=content.findViewById(R.id.posts_btn);
|
||||
actionButton=content.findViewById(R.id.profile_action_btn);
|
||||
notifyButton=content.findViewById(R.id.notify_btn);
|
||||
pager=content.findViewById(R.id.pager);
|
||||
scrollView=content.findViewById(R.id.scroller);
|
||||
tabbar=content.findViewById(R.id.tabbar);
|
||||
@@ -189,7 +191,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
nameEdit=content.findViewById(R.id.name_edit);
|
||||
bioEdit=content.findViewById(R.id.bio_edit);
|
||||
actionProgress=content.findViewById(R.id.action_progress);
|
||||
notifyProgress=content.findViewById(R.id.notify_progress);
|
||||
fab=content.findViewById(R.id.fab);
|
||||
followsYouView=content.findViewById(R.id.follows_you);
|
||||
|
||||
@@ -211,15 +212,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
};
|
||||
|
||||
tabViews=new FrameLayout[5];
|
||||
tabViews=new FrameLayout[4];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.profile_posts;
|
||||
case 1 -> R.id.profile_posts_with_replies;
|
||||
case 2 -> R.id.profile_pinned_posts;
|
||||
case 3 -> R.id.profile_media;
|
||||
case 4 -> R.id.profile_about;
|
||||
case 2 -> R.id.profile_media;
|
||||
case 3 -> R.id.profile_about;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -227,7 +227,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
pager.setOffscreenPageLimit(5);
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setAdapter(new ProfilePagerAdapter());
|
||||
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
|
||||
|
||||
@@ -243,9 +243,8 @@ 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.pinned_posts;
|
||||
case 3 -> R.string.media;
|
||||
case 4 -> R.string.profile_about;
|
||||
case 2 -> R.string.media;
|
||||
case 3 -> R.string.profile_about;
|
||||
default -> throw new IllegalStateException();
|
||||
});
|
||||
}
|
||||
@@ -260,7 +259,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
});
|
||||
|
||||
actionButton.setOnClickListener(this::onActionButtonClick);
|
||||
notifyButton.setOnClickListener(this::onNotifyButtonClick);
|
||||
avatar.setOnClickListener(this::onAvatarClick);
|
||||
cover.setOnClickListener(this::onCoverClick);
|
||||
refreshLayout.setOnRefreshListener(this);
|
||||
@@ -277,6 +275,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
|
||||
|
||||
username.setOnLongClickListener(v->{
|
||||
String username=account.acct;
|
||||
if(!username.contains("@")){
|
||||
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
}
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
|
||||
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
@@ -303,8 +313,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
postsFragment.onRefresh();
|
||||
if(postsWithRepliesFragment.loaded)
|
||||
postsWithRepliesFragment.onRefresh();
|
||||
if(pinnedPostsFragment.loaded)
|
||||
pinnedPostsFragment.onRefresh();
|
||||
if(mediaFragment.loaded)
|
||||
mediaFragment.onRefresh();
|
||||
}
|
||||
@@ -329,7 +337,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(postsFragment==null){
|
||||
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
|
||||
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
||||
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
|
||||
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
||||
aboutFragment=new ProfileAboutFragment();
|
||||
aboutFragment.setFields(fields);
|
||||
@@ -410,7 +417,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
|
||||
postsFragment.onApplyWindowInsets(childInsets);
|
||||
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
|
||||
pinnedPostsFragment.onApplyWindowInsets(childInsets);
|
||||
mediaFragment.onApplyWindowInsets(childInsets);
|
||||
}
|
||||
}
|
||||
@@ -461,7 +467,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
UiUtils.loadCustomEmojiInTextView(name);
|
||||
UiUtils.loadCustomEmojiInTextView(bio);
|
||||
|
||||
notifyButton.setVisibility(View.GONE);
|
||||
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
}else{
|
||||
@@ -529,26 +534,18 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
if(relationship==null && !isOwnProfile)
|
||||
return;
|
||||
inflater.inflate(R.menu.profile, menu);
|
||||
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
|
||||
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(getString(R.string.lists_with_user, account.getDisplayUsername()));
|
||||
if(isOwnProfile){
|
||||
for(int i=0;i<menu.size();i++){
|
||||
MenuItem item=menu.getItem(i);
|
||||
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks || item.getItemId()==R.id.manage_user_lists);
|
||||
}
|
||||
menu.findItem(R.id.favorites_list).setVisible(true);
|
||||
if(isOwnProfile)
|
||||
return;
|
||||
}
|
||||
|
||||
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
|
||||
if(relationship.following) {
|
||||
if(relationship.following)
|
||||
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
}else {
|
||||
else
|
||||
menu.findItem(R.id.hide_boosts).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()));
|
||||
else
|
||||
@@ -558,21 +555,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.share) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
if(id==R.id.share){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||
startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
}else if(id==R.id.bookmarks) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), BookmarksListFragment.class, args);
|
||||
}else if(id==R.id.favorites_list) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), FavoritesListFragment.class, args);
|
||||
}else if(id==R.id.mute){
|
||||
confirmToggleMuted();
|
||||
}else if(id==R.id.block){
|
||||
@@ -590,7 +577,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
updateRelationship();
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
@@ -604,12 +591,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}else if(id==R.id.manage_user_lists){
|
||||
final Bundle args=new Bundle();
|
||||
}else if(id==R.id.bookmarks){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("profileAccount", profileAccountID);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
Nav.go(getActivity(), ListTimelinesFragment.class, args);
|
||||
Nav.go(getActivity(), BookmarkedStatusListFragment.class, args);
|
||||
}else if(id==R.id.favorites){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -641,14 +630,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private void updateRelationship(){
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
UiUtils.setRelationshipToActionButton(relationship, notifyButton, true);
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
notifyButton.setSelected(relationship.notifying);
|
||||
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){
|
||||
@@ -687,9 +671,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return switch(page){
|
||||
case 0 -> postsFragment;
|
||||
case 1 -> postsWithRepliesFragment;
|
||||
case 2 -> pinnedPostsFragment;
|
||||
case 3 -> mediaFragment;
|
||||
case 4 -> aboutFragment;
|
||||
case 2 -> mediaFragment;
|
||||
case 3 -> aboutFragment;
|
||||
default -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
@@ -715,12 +698,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
|
||||
private void setNotifyProgressVisible(boolean visible){
|
||||
notifyButton.setTextVisible(!visible);
|
||||
notifyProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
notifyButton.setClickable(!visible);
|
||||
}
|
||||
|
||||
private void loadAccountInfoAndEnterEditMode(){
|
||||
if(editModeLoading)
|
||||
return;
|
||||
@@ -756,9 +733,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
invalidateOptionsMenu();
|
||||
pager.setUserInputEnabled(false);
|
||||
actionButton.setText(R.string.done);
|
||||
pager.setCurrentItem(4);
|
||||
pager.setCurrentItem(3);
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
for(int i=0;i<3;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
|
||||
tabbar.getTabAt(i).view.setEnabled(false);
|
||||
}
|
||||
@@ -799,7 +776,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
invalidateOptionsMenu();
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
for(int i=0;i<3;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
|
||||
}
|
||||
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
|
||||
@@ -817,7 +794,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
for(int i=0;i<3;i++){
|
||||
tabbar.getTabAt(i).view.setEnabled(true);
|
||||
}
|
||||
pager.setUserInputEnabled(true);
|
||||
@@ -889,10 +866,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return Collections.singletonList(att);
|
||||
}
|
||||
|
||||
private void onNotifyButtonClick(View v) {
|
||||
UiUtils.performToggleAccountNotifications(getActivity(), account, accountID, relationship, actionButton, this::setNotifyProgressVisible, this::updateRelationship);
|
||||
}
|
||||
|
||||
private void onAvatarClick(View v){
|
||||
if(isInEditMode){
|
||||
startImagePicker(AVATAR_RESULT);
|
||||
@@ -998,7 +971,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return loaded ? tabViews.length : 0;
|
||||
return loaded ? 4 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,7 +15,6 @@ 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.ImageButton;
|
||||
@@ -36,6 +35,7 @@ import org.joinmastodon.android.MainActivity;
|
||||
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.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -72,7 +72,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private PushSubscription pushSubscription;
|
||||
|
||||
private ImageView themeTransitionWindowView;
|
||||
private TextItem checkForUpdateItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -94,10 +93,6 @@ 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 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 HeaderItem(R.string.settings_behavior));
|
||||
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||
@@ -108,28 +103,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.useCustomTabs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
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.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 HeaderItem(R.string.home_timeline));
|
||||
items.add(new SwitchItem(R.string.settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||
GlobalUserPreferences.showReplies=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
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.settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
@@ -141,15 +114,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
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_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
||||
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 RedHeaderItem(R.string.settings_spicy));
|
||||
if (GithubSelfUpdater.needSelfUpdating()) {
|
||||
checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
|
||||
items.add(checkForUpdateItem);
|
||||
}
|
||||
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
||||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
@@ -198,7 +167,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(needUpdateNotificationSettings){
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
@@ -358,25 +327,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
checkForUpdateItem.loading = ev.state == GithubSelfUpdater.UpdateState.CHECKING;
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(checkForUpdateItem)) instanceof TextViewHolder tvh) tvh.rebind();
|
||||
|
||||
UpdateItem updateItem = null;
|
||||
if(items.get(0) instanceof UpdateItem item0) {
|
||||
updateItem = item0;
|
||||
} else if (ev.state != GithubSelfUpdater.UpdateState.CHECKING
|
||||
&& ev.state != GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
||||
updateItem = new UpdateItem();
|
||||
items.add(0, updateItem);
|
||||
list.setAdapter(new SettingsAdapter());
|
||||
}
|
||||
|
||||
if(updateItem != null && list.findViewHolderForAdapterPosition(0) instanceof UpdateViewHolder uvh){
|
||||
uvh.bind(updateItem);
|
||||
}
|
||||
|
||||
if (ev.state == GithubSelfUpdater.UpdateState.NO_UPDATE) {
|
||||
Toast.makeText(getActivity(), R.string.no_update_available, Toast.LENGTH_SHORT).show();
|
||||
if(items.get(0) instanceof UpdateItem item){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(0);
|
||||
if(holder instanceof UpdateViewHolder uvh){
|
||||
uvh.bind(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,16 +399,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private class TextItem extends Item{
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
private boolean loading;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick) {
|
||||
this(text, onClick, false);
|
||||
}
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
|
||||
public TextItem(@StringRes int text, Runnable onClick){
|
||||
this.text=getString(text);
|
||||
this.onClick=onClick;
|
||||
this.loading=loading;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -682,18 +631,14 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
private final ProgressBar progress;
|
||||
|
||||
public TextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text = itemView.findViewById(R.id.text);
|
||||
progress = itemView.findViewById(R.id.progress);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextItem item){
|
||||
text.setText(item.text);
|
||||
progress.animate().alpha(item.loading ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -748,9 +693,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
@Override
|
||||
public void onBind(UpdateItem item){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if (state == GithubSelfUpdater.UpdateState.CHECKING) return;
|
||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
text.setText(getString(R.string.update_available, info.version));
|
||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -10,7 +9,8 @@ import android.view.WindowInsets;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
|
||||
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
|
||||
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
||||
|
||||
@@ -66,8 +66,9 @@ public class SplashFragment extends AppKitFragment{
|
||||
|
||||
private void onButtonClick(View v){
|
||||
Bundle extras=new Bundle();
|
||||
extras.putBoolean("signup", v.getId()==R.id.btn_get_started);
|
||||
Nav.go(getActivity(), InstanceCatalogFragment.class, extras);
|
||||
boolean isSignup=v.getId()==R.id.btn_get_started;
|
||||
extras.putBoolean("signup", isSignup);
|
||||
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
|
||||
}
|
||||
|
||||
private void updateArtSize(int w, int h){
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
@@ -18,6 +19,8 @@ import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
@@ -134,6 +137,40 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
List<Status> toRemove=Stream.concat(data.stream(), preloadedData.stream())
|
||||
.filter(s->s.account.id.equals(ev.postsByAccountID) || (s.reblog!=null && s.reblog.account.id.equals(ev.postsByAccountID)))
|
||||
.collect(Collectors.toList());
|
||||
for(Status s:toRemove){
|
||||
removeStatus(s);
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeStatus(Status status){
|
||||
data.remove(status);
|
||||
preloadedData.remove(status);
|
||||
int index=-1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
if(status.id.equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(index==-1)
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(status.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
}
|
||||
|
||||
public class EventListener{
|
||||
|
||||
@Subscribe
|
||||
@@ -165,28 +202,13 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
Status status=getStatusByID(ev.id);
|
||||
if(status==null)
|
||||
return;
|
||||
data.remove(status);
|
||||
preloadedData.remove(status);
|
||||
int index=-1;
|
||||
for(int i=0;i<displayItems.size();i++){
|
||||
if(ev.id.equals(displayItems.get(i).parentID)){
|
||||
index=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(index==-1)
|
||||
return;
|
||||
int lastIndex;
|
||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
||||
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
|
||||
break;
|
||||
}
|
||||
displayItems.subList(index, lastIndex).clear();
|
||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||
removeStatus(status);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
StatusListFragment.this.onStatusCreated(ev);
|
||||
}
|
||||
|
||||
@@ -206,5 +228,14 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
if(ev.isUnfollow && !shouldRemoveAccountPostsWhenUnfollowing())
|
||||
return;
|
||||
StatusListFragment.this.onRemoveAccountPostsEvent(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ public class ThreadFragment extends StatusListFragment{
|
||||
return statuses;
|
||||
return statuses.stream().filter(status->{
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status.getContentStatus().content))
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -286,7 +286,6 @@ 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.lists_with_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
@@ -354,7 +353,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
bindRelationship();
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
|
||||
@@ -19,7 +19,6 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.ScrollableToTop;
|
||||
import org.joinmastodon.android.fragments.ListTimelinesFragment;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
@@ -52,8 +51,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
private DiscoverAccountsFragment accountsFragment;
|
||||
private SearchFragment searchFragment;
|
||||
private LocalTimelineFragment localTimelineFragment;
|
||||
private FederatedTimelineFragment federatedTimelineFragment;
|
||||
private ListTimelinesFragment listTimelinesFragment;
|
||||
|
||||
private String accountID;
|
||||
private Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||
@@ -75,17 +72,15 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[7];
|
||||
tabViews=new FrameLayout[5];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.discover_local_timeline;
|
||||
case 1 -> R.id.discover_federated_timeline;
|
||||
case 2 -> R.id.discover_hashtags;
|
||||
case 3 -> R.id.discover_posts;
|
||||
case 4 -> R.id.discover_news;
|
||||
case 5 -> R.id.discover_users;
|
||||
case 6 -> R.id.discover_lists;
|
||||
case 0 -> R.id.discover_posts;
|
||||
case 1 -> R.id.discover_hashtags;
|
||||
case 2 -> R.id.discover_news;
|
||||
case 3 -> R.id.discover_local_timeline;
|
||||
case 4 -> R.id.discover_users;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -111,7 +106,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
});
|
||||
|
||||
if(localTimelineFragment==null){
|
||||
if(postsFragment==null){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
@@ -131,20 +126,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
localTimelineFragment=new LocalTimelineFragment();
|
||||
localTimelineFragment.setArguments(args);
|
||||
|
||||
federatedTimelineFragment=new FederatedTimelineFragment();
|
||||
federatedTimelineFragment.setArguments(args);
|
||||
|
||||
listTimelinesFragment=new ListTimelinesFragment();
|
||||
listTimelinesFragment.setArguments(args);
|
||||
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.add(R.id.discover_posts, postsFragment)
|
||||
.add(R.id.discover_local_timeline, localTimelineFragment)
|
||||
.add(R.id.discover_federated_timeline, federatedTimelineFragment)
|
||||
.add(R.id.discover_hashtags, hashtagsFragment)
|
||||
.add(R.id.discover_news, newsFragment)
|
||||
.add(R.id.discover_users, accountsFragment)
|
||||
.add(R.id.discover_lists, listTimelinesFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@@ -152,13 +139,11 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
@Override
|
||||
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
|
||||
tab.setText(switch(position){
|
||||
case 0 -> R.string.local_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.list_timelines;
|
||||
case 0 -> R.string.posts;
|
||||
case 1 -> R.string.hashtags;
|
||||
case 2 -> R.string.news;
|
||||
case 3 -> R.string.local_timeline;
|
||||
case 4 -> R.string.for_you;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
});
|
||||
tab.view.textView.setAllCaps(true);
|
||||
@@ -244,8 +229,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
}
|
||||
|
||||
public void loadData(){
|
||||
if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
|
||||
localTimelineFragment.loadData();
|
||||
if(postsFragment!=null && !postsFragment.loaded && !postsFragment.dataLoading)
|
||||
postsFragment.loadData();
|
||||
}
|
||||
|
||||
private void onSearchEditFocusChanged(View v, boolean hasFocus){
|
||||
@@ -281,13 +266,11 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
return switch(page){
|
||||
case 0 -> localTimelineFragment;
|
||||
case 1 -> federatedTimelineFragment;
|
||||
case 2 -> hashtagsFragment;
|
||||
case 3 -> postsFragment;
|
||||
case 4 -> newsFragment;
|
||||
case 5 -> accountsFragment;
|
||||
case 6 -> listTimelinesFragment;
|
||||
case 0 -> postsFragment;
|
||||
case 1 -> hashtagsFragment;
|
||||
case 2 -> newsFragment;
|
||||
case 3 -> localTimelineFragment;
|
||||
case 4 -> accountsFragment;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FederatedTimelineFragment extends StatusListFragment{
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(res.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following);
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
|
||||
case STATUS -> {
|
||||
Status status=res.status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
|
||||
@@ -107,7 +107,7 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,30 @@ 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;
|
||||
|
||||
@@ -53,54 +37,48 @@ import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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;
|
||||
|
||||
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;
|
||||
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();
|
||||
|
||||
private static final double DUNBAR=Math.log(800);
|
||||
|
||||
public InstanceCatalogFragment(){
|
||||
super(R.layout.fragment_onboarding_common, 10);
|
||||
public InstanceCatalogFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,258 +87,9 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
||||
isSignup=getArguments().getBoolean("signup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context){
|
||||
super.onAttach(context);
|
||||
setRefreshEnabled(false);
|
||||
loadData();
|
||||
}
|
||||
protected abstract void proceedWithAuthOrSignup(Instance instance);
|
||||
|
||||
@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){
|
||||
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){
|
||||
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
|
||||
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
||||
return true;
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||
@@ -376,60 +105,73 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onSearchChangedDebounced(){
|
||||
protected void onSearchChangedDebounced(){
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||
updateFilteredList();
|
||||
loadInstanceInfo(currentSearchQuery, false);
|
||||
}
|
||||
|
||||
private void updateFilteredList(){
|
||||
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
|
||||
filteredData.clear();
|
||||
for(CatalogInstance instance:data){
|
||||
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
|
||||
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
|
||||
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
|
||||
filteredData.add(instance);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
DiffUtil.calculateDiff(new DiffUtil.Callback(){
|
||||
@Override
|
||||
public int getOldListSize(){
|
||||
return prevData.size();
|
||||
// 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize(){
|
||||
return filteredData.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 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);
|
||||
return group;
|
||||
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
|
||||
return sortedList;
|
||||
}
|
||||
|
||||
private void showProgressDialog(){
|
||||
protected abstract void updateFilteredList();
|
||||
|
||||
protected void showProgressDialog(){
|
||||
instanceProgressDialog=new ProgressDialog(getActivity());
|
||||
instanceProgressDialog.setMessage(getString(R.string.loading_instance));
|
||||
instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo());
|
||||
instanceProgressDialog.show();
|
||||
}
|
||||
|
||||
private String normalizeInstanceDomain(String _domain){
|
||||
protected 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;
|
||||
}
|
||||
@@ -444,12 +186,12 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
||||
return domain;
|
||||
}
|
||||
|
||||
private void loadInstanceInfo(String _domain, boolean isFromRedirect){
|
||||
protected 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))
|
||||
for(CatalogInstance ci : filteredData){
|
||||
if(ci.domain.equals(domain) && ci!=fakeInstance)
|
||||
return;
|
||||
}
|
||||
CatalogInstance ci=cachedInstance.toCatalogInstance();
|
||||
@@ -467,44 +209,57 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
||||
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(domain.equals(currentSearchQuery) || currentSearchQuery.equals(redirects.get(domain)) || currentSearchQuery.equals(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);
|
||||
}
|
||||
@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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
@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();
|
||||
}
|
||||
loadingInstanceDomain=null;
|
||||
showInstanceInfoLoadError(domain, error);
|
||||
}
|
||||
}).execNoAuth(domain);
|
||||
}
|
||||
}
|
||||
}).execNoAuth(domain);
|
||||
}
|
||||
|
||||
private void cancelLoadingInstanceInfo(){
|
||||
@@ -575,7 +330,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
||||
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}")){
|
||||
@@ -607,78 +362,26 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@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 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(CatalogInstance item){
|
||||
title.setText(item.normalizedDomain);
|
||||
description.setText(item.description);
|
||||
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
|
||||
lang.setText(item.language.toUpperCase());
|
||||
radioButton.setChecked(chosenInstance==item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(chosenInstance==item)
|
||||
return;
|
||||
if(chosenInstance!=null){
|
||||
int idx=filteredData.indexOf(chosenInstance);
|
||||
if(idx!=-1){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
|
||||
if(holder instanceof InstanceViewHolder ivh){
|
||||
ivh.radioButton.setChecked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
radioButton.setChecked(true);
|
||||
if(chosenInstance==null)
|
||||
nextButton.setEnabled(true);
|
||||
chosenInstance=item;
|
||||
loadInstanceInfo(chosenInstance.domain, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.LocaleList;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.catalog.CatalogCategory;
|
||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
|
||||
private View headerView;
|
||||
private MastodonAPIRequest<?> getCategoriesRequest;
|
||||
private TabLayout categoriesList;
|
||||
private String currentCategory="all";
|
||||
private List<CatalogCategory> categories=new ArrayList<>();
|
||||
|
||||
|
||||
public InstanceCatalogSignupFragment(){
|
||||
super(R.layout.fragment_onboarding_common, 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context){
|
||||
super.onAttach(context);
|
||||
setRefreshEnabled(false);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetCatalogInstances(null, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<CatalogInstance> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(sortInstances(result), false);
|
||||
updateFilteredList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
onDataLoaded(Collections.emptyList(), false);
|
||||
}
|
||||
})
|
||||
.execNoAuth("");
|
||||
getCategoriesRequest=new GetCatalogCategories(null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<CatalogCategory> result){
|
||||
getCategoriesRequest=null;
|
||||
CatalogCategory all=new CatalogCategory();
|
||||
all.category="all";
|
||||
categories.add(all);
|
||||
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
|
||||
updateCategories();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getCategoriesRequest=null;
|
||||
error.showToast(getActivity());
|
||||
CatalogCategory all=new CatalogCategory();
|
||||
all.category="all";
|
||||
categories.add(all);
|
||||
updateCategories();
|
||||
}
|
||||
})
|
||||
.execNoAuth("");
|
||||
}
|
||||
|
||||
private void updateCategories(){
|
||||
categoriesList.removeAllTabs();
|
||||
for(CatalogCategory cat:categories){
|
||||
int titleRes=getTitleForCategory(cat.category);
|
||||
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
|
||||
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
|
||||
emoji.setImageResource(getEmojiForCategory(cat.category));
|
||||
categoriesList.addTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(getCategoriesRequest!=null)
|
||||
getCategoriesRequest.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
|
||||
searchEdit=headerView.findViewById(R.id.search_edit);
|
||||
categoriesList=headerView.findViewById(R.id.categories_list);
|
||||
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab){
|
||||
CatalogCategory category=categories.get(tab.getPosition());
|
||||
currentCategory=category.category;
|
||||
updateFilteredList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab){
|
||||
|
||||
}
|
||||
});
|
||||
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
|
||||
searchEdit.addTextChangedListener(new TextWatcher(){
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
searchEdit.removeCallbacks(searchDebouncer);
|
||||
searchEdit.postDelayed(searchDebouncer, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
}
|
||||
});
|
||||
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void proceedWithAuthOrSignup(Instance instance){
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
if(isSignup){
|
||||
if(!instance.registrations){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.instance_signup_closed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}else{
|
||||
}
|
||||
}
|
||||
|
||||
// private String getEmojiForCategory(String category){
|
||||
// return switch(category){
|
||||
// case "all" -> "💬";
|
||||
// case "academia" -> "📚";
|
||||
// case "activism" -> "✊";
|
||||
// case "food" -> "🍕";
|
||||
// case "furry" -> "🦁";
|
||||
// case "games" -> "🕹";
|
||||
// case "general" -> "🐘";
|
||||
// case "journalism" -> "📰";
|
||||
// case "lgbt" -> "🏳️🌈";
|
||||
// case "regional" -> "📍";
|
||||
// case "art" -> "🎨";
|
||||
// case "music" -> "🎼";
|
||||
// case "tech" -> "📱";
|
||||
// default -> "❓";
|
||||
// };
|
||||
// }
|
||||
|
||||
private int getEmojiForCategory(String category){
|
||||
return switch(category){
|
||||
case "all" -> R.drawable.ic_category_all;
|
||||
case "academia" -> R.drawable.ic_category_academia;
|
||||
case "activism" -> R.drawable.ic_category_activism;
|
||||
case "food" -> R.drawable.ic_category_food;
|
||||
case "furry" -> R.drawable.ic_category_furry;
|
||||
case "games" -> R.drawable.ic_category_games;
|
||||
case "general" -> R.drawable.ic_category_general;
|
||||
case "journalism" -> R.drawable.ic_category_journalism;
|
||||
case "lgbt" -> R.drawable.ic_category_lgbt;
|
||||
case "regional" -> R.drawable.ic_category_regional;
|
||||
case "art" -> R.drawable.ic_category_art;
|
||||
case "music" -> R.drawable.ic_category_music;
|
||||
case "tech" -> R.drawable.ic_category_tech;
|
||||
default -> R.drawable.ic_category_unknown;
|
||||
};
|
||||
}
|
||||
|
||||
private int getTitleForCategory(String category){
|
||||
return switch(category){
|
||||
case "all" -> R.string.category_all;
|
||||
case "academia" -> R.string.category_academia;
|
||||
case "activism" -> R.string.category_activism;
|
||||
case "food" -> R.string.category_food;
|
||||
case "furry" -> R.string.category_furry;
|
||||
case "games" -> R.string.category_games;
|
||||
case "general" -> R.string.category_general;
|
||||
case "journalism" -> R.string.category_journalism;
|
||||
case "lgbt" -> R.string.category_lgbt;
|
||||
case "regional" -> R.string.category_regional;
|
||||
case "art" -> R.string.category_art;
|
||||
case "music" -> R.string.category_music;
|
||||
case "tech" -> R.string.category_tech;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateFilteredList(){
|
||||
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
|
||||
filteredData.clear();
|
||||
for(CatalogInstance instance:data){
|
||||
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
|
||||
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
|
||||
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
|
||||
filteredData.add(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
DiffUtil.calculateDiff(new DiffUtil.Callback(){
|
||||
@Override
|
||||
public int getOldListSize(){
|
||||
return prevData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize(){
|
||||
return filteredData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
|
||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
|
||||
return prevData.get(oldItemPosition)==filteredData.get(newItemPosition);
|
||||
}
|
||||
}).dispatchUpdatesTo(adapter);
|
||||
}
|
||||
|
||||
|
||||
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
|
||||
public InstancesAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InstanceCatalogSignupFragment.InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new InstanceCatalogSignupFragment.InstanceViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(InstanceCatalogSignupFragment.InstanceViewHolder holder, int position){
|
||||
holder.bind(filteredData.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return filteredData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title, description, userCount, lang;
|
||||
private final RadioButton radioButton;
|
||||
|
||||
public InstanceViewHolder(){
|
||||
super(getActivity(), R.layout.item_instance_catalog, list);
|
||||
title=findViewById(R.id.title);
|
||||
description=findViewById(R.id.description);
|
||||
userCount=findViewById(R.id.user_count);
|
||||
lang=findViewById(R.id.lang);
|
||||
radioButton=findViewById(R.id.radiobtn);
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(CatalogInstance item){
|
||||
title.setText(item.normalizedDomain);
|
||||
description.setText(item.description);
|
||||
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
|
||||
lang.setText(item.language.toUpperCase());
|
||||
radioButton.setChecked(chosenInstance==item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(chosenInstance==item)
|
||||
return;
|
||||
if(chosenInstance!=null){
|
||||
int idx=filteredData.indexOf(chosenInstance);
|
||||
if(idx!=-1){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
|
||||
if(holder instanceof InstanceCatalogSignupFragment.InstanceViewHolder ivh){
|
||||
ivh.radioButton.setChecked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
radioButton.setChecked(true);
|
||||
if(chosenInstance==null)
|
||||
nextButton.setEnabled(true);
|
||||
chosenInstance=item;
|
||||
loadInstanceInfo(chosenInstance.domain, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
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){
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
radioButton.setChecked(true);
|
||||
if(chosenInstance==null)
|
||||
nextButton.setEnabled(true);
|
||||
chosenInstance=item;
|
||||
loadInstanceInfo(chosenInstance.domain, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -131,7 +132,10 @@ public class InstanceRulesFragment extends AppKitFragment{
|
||||
|
||||
@Override
|
||||
public void onBind(Instance.Rule item){
|
||||
title.setText(item.text);
|
||||
if(item.parsedText==null){
|
||||
item.parsedText=HtmlParser.parseLinks(item.text);
|
||||
}
|
||||
title.setText(item.parsedText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
@@ -125,11 +127,12 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
|
||||
private void onUnfollowClick(){
|
||||
new SetAccountFollowed(reportAccount.id, false, false, false)
|
||||
new SetAccountFollowed(reportAccount.id, false, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
Nav.finish(ReportDoneFragment.this);
|
||||
E.post(new RemoveAccountPostsEvent(accountID, reportAccount.id, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -50,6 +50,10 @@ public class Filter extends BaseModel{
|
||||
return pattern.matcher(text).find();
|
||||
}
|
||||
|
||||
public boolean matches(Status status){
|
||||
return matches(status.getContentStatus().getStrippedText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Filter{"+
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user