Compare commits
No commits in common. "master" and "m3-merger" have entirely different histories.
1193 changed files with 9377 additions and 39044 deletions
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
@ -1,11 +1,12 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: LucasGGamerM
|
||||
custom: ["https://liberapay.com/LucasGGamerM/donate", liberapay.com]
|
||||
patreon: # mastodon
|
||||
open_collective: # Replace with a single Open Collective 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: LucasGGamerM # Replace with a single Liberapay username e.g., user1
|
||||
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']
|
||||
|
|
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -25,7 +25,7 @@ Does this issue also occur with the respective upstream release?
|
|||
> No / Yes
|
||||
|
||||
> In case it does, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead.
|
||||
> If this bug is seriously impacting your usage or you think I might want to try to fix it for Moshidon, feel free to still create this issue!
|
||||
> If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue!
|
||||
|
||||
**Screenshots and screen recordings**
|
||||
|
||||
|
|
16
.github/workflows/mirror-to-codeberg.yml
vendored
16
.github/workflows/mirror-to-codeberg.yml
vendored
|
@ -1,16 +0,0 @@
|
|||
name: Mirror to Codeberg
|
||||
|
||||
on: [push]
|
||||
jobs:
|
||||
sync-git:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: yesolutions/mirror-action@master
|
||||
with:
|
||||
REMOTE: 'https://codeberg.org/LucasGGamerM/moshidon.git'
|
||||
GIT_USERNAME: LucasGGamerM
|
||||
GIT_PASSWORD: ${{ secrets.CODEBERG_GIT_PASSWORD }}
|
45
.github/workflows/nightly-builds.yml
vendored
45
.github/workflows/nightly-builds.yml
vendored
|
@ -3,7 +3,6 @@ name: Nightly builds
|
|||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -11,27 +10,27 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# - name: Checkout Appkit Repo
|
||||
# uses: actions/checkout@v3
|
||||
# with:
|
||||
# repository: grishka/appkit
|
||||
#
|
||||
# - name: set up JDK 17
|
||||
# uses: actions/setup-java@v3
|
||||
# with:
|
||||
# java-version: '17'
|
||||
# distribution: 'corretto'
|
||||
# cache: gradle
|
||||
#
|
||||
# - name: Comment out signing config in appkits gradle file
|
||||
# run: |
|
||||
# sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
||||
#
|
||||
# - name: Grant execute permission for gradlew for Appkit
|
||||
# run: chmod +x gradlew
|
||||
#
|
||||
# - name: Compile appkit
|
||||
# run: ./gradlew publishToMavenLocal
|
||||
- name: Checkout Appkit Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: grishka/appkit
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'corretto'
|
||||
cache: gradle
|
||||
|
||||
- name: Comment out signing config in appkits gradle file
|
||||
run: |
|
||||
sed -i 's/sign publishing\.publications\.release/\/\/ sign publishing.publications.release/' appkit/maven-push.gradle
|
||||
|
||||
- name: Grant execute permission for gradlew for Appkit
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Compile appkit
|
||||
run: ./gradlew publishToMavenLocal
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: set up JDK 17
|
||||
|
@ -65,7 +64,7 @@ jobs:
|
|||
CURRENT_DATE: ${{ steps.date.outputs.date }}
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
with:
|
||||
name: moshidon-nightly.apk
|
||||
path: ./mastodon/build/outputs/apk/nightly/moshidon-nightly.apk
|
||||
|
|
56
FAQ.md
56
FAQ.md
|
@ -4,60 +4,6 @@ Q: What are the main differences between Moshidon and Megalodon?
|
|||
|
||||
A: There are many, but the most outstanding differences are: the ability to have other server's local timeline inside the app. It can be acessed in the "Add community" option in the top right corner of the Edit timelines screen. Other outstanding features that Moshidon has are some quality of life improvements, such as notification actions and allowing for unlisted replies by default. Most other features are pretty minor, such as profile notes directly available in the person's profile. Other features are quite minor usability and visibility improvements. All of which can be found in the settings page.
|
||||
|
||||
Q: Will there ever be a version of Moshidon for iOS?
|
||||
Q: Will there ever be a versjon of Moshidon for iOS?
|
||||
|
||||
A: No. As android and iOS apps do not share code, it is incredibly hard to port.
|
||||
|
||||
## Detailed changes
|
||||
|
||||
### Features
|
||||
|
||||
* [Adding the ability to view other server's local timelines](https://github.com/LucasGGamerM/moshidon/tree/feature/local-timelines)
|
||||
* [Adding the ability to load followers and following from remote instance](https://github.com/LucasGGamerM/moshidon/tree/feature/remote-followers)
|
||||
* [Adding the ability to have filtered posts show with a warning](https://github.com/LucasGGamerM/moshidon/tree/feature/filters_again)
|
||||
* [Add “Unlisted” as a post visibility option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
|
||||
* Adding a useful private profile note box
|
||||
* Auto hiding the compose button on scroll
|
||||
* Adding the ability to remind yourself to add alt text to images
|
||||
* An indicator for if an image has alt text or not
|
||||
* Adding the ability to have drafts
|
||||
* Also adding the ability to view announcements from your instance
|
||||
* Adding the ability to post for local timeline only (Only on instances that support it!)
|
||||
* [Add image description button and viewer](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon: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:megalodon: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:megalodon: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:megalodon:feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
|
||||
* [Add “Check for update” button in addition to integrated update checker](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/check-for-update-button)
|
||||
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/mark-media-as-sensitive)
|
||||
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
|
||||
* [Follow and unfollow hashtags](https://github.com/sk22/megalodon/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
|
||||
* [Notification bell for posts](https://github.com/sk22/megalodon/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
|
||||
* [Viewing lists and adding/removing users from lists](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
|
||||
* [List favorited posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/favs-list)
|
||||
* [Accept/reject follow requests](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/follow-requests)
|
||||
* [Display content warning title above text](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
|
||||
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
|
||||
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-reply-visibility)
|
||||
* [Clickable reply/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
|
||||
* [Clickable reply line while replying to open original post](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/clickable-reply-line-compose)
|
||||
|
||||
|
||||
### Behavior
|
||||
|
||||
* Ask for confirmation before reblogging
|
||||
* Adding a bottom option for the publish button, allowing for easier use on larger screens!
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon: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:megalodon: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:megalodon: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:megalodon:spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
|
||||
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
|
||||
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
|
||||
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
|
||||
|
||||
|
||||
### Visual
|
||||
|
||||
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
|
||||
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
|
||||
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)
|
||||
|
|
233
README.md
233
README.md
|
@ -1,91 +1,183 @@
|
|||
#  Moshidon, the material you mastodon client!
|
||||

|
||||
|
||||
# Moshidon, the material you mastodon client!
|
||||
|
||||
> A fork of [megalodon](https://github.com/sk22/megalodon) which is a fork of [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly won’t ever be implemented, such as the federated timeline, unlisted posting, bookmarks and an image description viewer.
|
||||
|
||||
|
||||
> A fast, highly customizable, up-to-date fork of [megalodon](https://github.com/sk22/megalodon) adding important features such as a fully federated timeline, unlisted posting, drafts, scheduled posts, bookmarks, and alt text warnings.
|
||||
[](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk)
|
||||
|
||||
[](https://github.com/LucasGGamerM/moshidon-nightly/releases/latest/download/moshidon-nightly.apk)
|
||||
|
||||
|
||||
## Download Now
|
||||
[](https://translate.codeberg.org/engage/moshidon/)
|
||||
|
||||
[](https://github.com/LucasGGamerM/moshidon/actions/workflows/nightly-builds.yml)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.moshinda"><img height="35" alt="Get it on Google Play" src="img/google-play-badge.png"></a> <a href="https://f-droid.org/pt_BR/packages/org.joinmastodon.android.moshinda"><img height="35" alt="Get it on F-Droid" src="img/f-droid-badge.png"></a> <a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="35" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android.moshinda"><img height="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
|
||||
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda"><img height="50" alt="Get it on IzzyOnDroid" src="img/izzy-badge.png"></a>
|
||||
|
||||
[](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk) [](https://translate.codeberg.org/engage/moshidon/) [](https://github.com/LucasGGamerM/moshidon-nightly/releases/latest/download/moshidon-nightly.apk) [](https://github.com/LucasGGamerM/moshidon/actions/workflows/nightly-builds.yml)
|
||||
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
|
||||
### We also support LiberaPay at: https://liberapay.com/LucasGGamerM/donate (Currently broken)
|
||||
|
||||
## Donate
|
||||
### You can also donate some Monero through this wallet address as well:
|
||||
4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j
|
||||
|
||||
<a href="https://github.com/sponsors/LucasGGamerM">Github Sponsors</a> | <a href="https://liberapay.com/LucasGGamerM/donate">Liberapay</a> | Monero Wallet Key: `4886mdarcyB6Yf8Qc6vDJBK1fz6ibHFLZUmHb4GZZz9yLGNhcG3XC64e5UZ8dVQYTLZb82W6P9WhteowW4STJEec97Gf22j`
|
||||
---
|
||||
|
||||
## Key Features
|
||||
## Key features
|
||||
|
||||
[ screenshot of full timeline in default colour scheme ]
|
||||
[ screenshot of full timeline in an alt colour scheme ]
|
||||
[ screenshot of profile page ]
|
||||
[ screenshot of compose post window ]
|
||||
### **The ability to add other server's local timeline to your timelines**
|
||||
|
||||
### Flexible Timelines
|
||||
It can be accessed in the "Edit timelines" menu, where you can add a new "Community" to see other server's local posts!
|
||||
|
||||
[ Home dropdown menu ]
|
||||
### **View remote profiles**
|
||||
|
||||
Under the Home menu by default you can see your active account's timeline, your server's local timeline, and your server's federated timeline. You can also pin hashtags, lists, other servers, or make a custom view of just your posts, your bookmarks, or your favourites for quick access. Then sort these timelines to prioritize the ones you visit most often.
|
||||
You can now see all of a profile follows and followers, by directly loading them from the profile's home instance. In case of a failed lookup, the app will automatically fall back to the older method.
|
||||
|
||||
### Multiple Accounts & Crossposting
|
||||
### **Translate posts easily**
|
||||
|
||||
Sign in to multiple accounts in the same app and easily switch between them. Press and hold on the boost or fave button to boost or fave a post to a different account than the one you are currently browsing with.
|
||||
Allows you to easily translate posts in another language with a translate button! Your instance must support translation, otherwise it will not work.
|
||||
|
||||
[ boost icon pop up select profile ]
|
||||
### **Show posts filtered with a warning**
|
||||
|
||||
### Drafts & Scheduled Posts
|
||||
Allows you to have filtered posts collapsed with a warning! As shown in the screenshots:
|
||||
|
||||
Write posts and save them, or schedule them to post later. Edit and delete your drafts.
|
||||
|
||||
### Alt Text Tag & Reminder
|
||||
|
||||
An unobtrusive ALT tag appears on images with alt text. Clicking on the icon makes the alt text appear. By default, Moshidon will show a warning to add alt text if your post has any attachments lacking alt text. This is for better accessibility, and it can be disabled in settings. You can also hide from your feed all posts that are lacking in alt text.
|
||||
|
||||
[ image with alt text icon higlighted ]
|
||||
[ alt text expanded ]
|
||||
|
||||
### Themes & Customization
|
||||
|
||||
Moshidon is designed according to Material Design principles. Follow your device's light or dark mode settings or change colour palette - your system's default, purple, black & white, "pitch black" (battery saving) and more. Customize your experience by moving or renaming the publish button, show or hide sensitive media by default, reduce motion, collapse long posts, add haptic feedback, or making the fave button a heart ♥ or a star ★.
|
||||
|
||||
### Not Just For Mastodon
|
||||
|
||||
Supports features available on other types of fediverse servers such as admin announcements, showing pronouns in user names, post translation, emoji reactions, local-only posting, and markdown or html in posts.
|
||||
|
||||
### Fully Federated Feed & Profiles
|
||||
|
||||
See all public posts from servers your server federates with and fetch profiles from a user's local server for accurate up to date information.
|
||||
|
||||
## And more...
|
||||
- quote-posts - links to fediverse posts in other posts will be loaded inline like quote-tweets
|
||||
- manage pinned posts and bookmarks
|
||||
- manage lists, filters, and most privacy settings
|
||||
- display pronouns in timelines, threads, and user listings
|
||||
- get only specific types of notifications (no more finished polls!), limit who you get notifications from, or group all notifications into one.
|
||||
- automatically add "re:" to beginning of replies with content warnings
|
||||
- ask before boosting or deleting posts
|
||||
- when replying to a boosted post automatically mention the person who boosted it
|
||||
- overlay audio from posts, allowing your existing media to keep playing
|
||||
- auto-reveal CWs that are the same as ones you've already opened, or always reveal content warnings and sensitive media
|
||||
- hide media previews in timelines (save data)
|
||||
- show post interaction counts in timeline
|
||||
- allow custom emoji in display names
|
||||
- enable scrolling text for long display names
|
||||
- hide interaction buttons
|
||||
- show post dividers
|
||||
Before | After
|
||||
:-------------------------:|:-------------------------:
|
||||
 | 
|
||||
|
||||
|
||||
## Installation & Releases
|
||||
### **Color themes**
|
||||
|
||||
Moshidon is available on GitHub, Google Play, F-Droid, and the IzzyOnDroid repo. All sources provide the same ` moshidon.apk ` stable release. Older releases are available on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
|
||||
Allows you to change theme within the app. Supports Material You, purple, pink, green, blue, red, orange, yellow and Nord!
|
||||
|
||||
### How to Install from GitHub
|
||||
[Download the latest stable release from Github](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk) and open it. You might have to accept installing APK files from your browser. Moshidon will automatically check for new updates available on GitHub and offer to download and install them within the app. You can also manually press “Check for updates” at the bottom of the settings page.
|
||||
### **Unlisted posting**
|
||||
|
||||
### Nightly Version
|
||||
All ` moshidon-night.apk ` nightly builds can be downloaded on the [Nightly Releases](https://github.com/LucasGGamerM/moshidon-nightly/releases) page. This is an unstable version with an integrated updater for development and testing purposes. If you find any bugs with it, please file a bug report on our [Issues](https://github.com/LucasGGamerM/moshidon/issues) page.
|
||||
**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”).**
|
||||
|
||||
## Building & Contributing
|
||||
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 neighborhoods your home instance is connected to.**
|
||||
|
||||
Despite being one of the main features of federated social media, the Federated timeline wasn’t included in the official Mastodon app – supposedly, because this conflicts with Google’s safety requirements for apps on the Play Store.
|
||||
|
||||
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!
|
||||
|
||||
### **Reminder to add alt text to attached media**
|
||||
|
||||
By default, Moshidon will show a warning to add alt text if your post has any attachments without any alt text. This is for better accessibility, and it can easily be bypassed and disabled in settings.
|
||||
|
||||
### **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. Moshidon 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/LucasGGamerM/moshidon/releases/latest/download/moshidon.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/LucasGGamerM/moshidon/releases) page.
|
||||
|
||||
Moshidon makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)’s automatic update checker. Moshidon 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!
|
||||
|
||||
Moshidon is also available in [IzzyOnDroid repo](https://apt.izzysoft.de/fdroid/index/apk/org.joinmastodon.android.moshinda), compatible with all F-Droid clients. The APK provided here is the same as the one included in the Releases.
|
||||
|
||||
## Release variants
|
||||
|
||||
### Stable variant
|
||||
|
||||
All stable version downloads can be found on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
|
||||
|
||||
**`moshidon.apk`**
|
||||
|
||||
Variant with an integrated updater. If you download Moshidon from here (and not from an app store), just download the regular `moshidon.apk`.
|
||||
|
||||
### Nightly variant
|
||||
|
||||
All nightly builds can be downloaded at [Nightly Releases](https://github.com/LucasGGamerM/moshidon-nightly/releases) page.
|
||||
|
||||
**`moshidon-nightly.apk`**
|
||||
|
||||
Unstable variant with an integrated updater. It's for development and testing purposes. If you find any bugs with it, please file a bug report at our [issues](https://github.com/LucasGGamerM/moshidon/issues) page.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Detailed changes
|
||||
|
||||
### Features
|
||||
|
||||
* [Adding the ability to view other server's local timelines](https://github.com/LucasGGamerM/moshidon/tree/feature/local-timelines)
|
||||
* [Adding the ability to load followers and following from remote instance](https://github.com/LucasGGamerM/moshidon/tree/feature/remote-followers)
|
||||
* [Adding the ability to have filtered posts show with a warning](https://github.com/LucasGGamerM/moshidon/tree/feature/filters_again)
|
||||
* [Add “Unlisted” as a post visibility option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/enable-unlisted)
|
||||
([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
|
||||
* Adding a useful private profile note box
|
||||
* Auto hiding the compose button on scroll
|
||||
* Adding the ability to remind yourself to add alt text to images
|
||||
* An indicator for if an image has alt text or not
|
||||
* Adding the ability to have drafts
|
||||
* Also adding the ability to view announcements from your instance
|
||||
* Adding the ability to post for local timeline only (Only on instances that support it!)
|
||||
* [Add image description button and viewer](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon: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:megalodon: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:megalodon: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:megalodon:feature/bookmarks) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/22))
|
||||
* [Add “Check for update” button in addition to integrated update checker](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/check-for-update-button)
|
||||
* [Add “Mark media as sensitive” option](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/mark-media-as-sensitive)
|
||||
* [Add settings to hide replies and reposts from the timeline](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/filter-home-timeline) ([Pull request](https://github.com/mastodon/mastodon-android/pull/317))
|
||||
* [Follow and unfollow hashtags](https://github.com/sk22/megalodon/commit/7d38f031f197aa6cefaf53e39d929538689c1e4e) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/233))
|
||||
* [Notification bell for posts](https://github.com/sk22/megalodon/commit/b166ca705eb9169025ef32bbe6315b42491b57ea) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/81))
|
||||
* [Viewing lists and adding/removing users from lists](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:list-timeline-views) based on [@obstsalatschuessel](https://github.com/obstsalatschuessel)'s [Pull request](https://github.com/mastodon/mastodon-android/pull/286)
|
||||
* [List favorited posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/favs-list)
|
||||
* [Accept/reject follow requests](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/follow-requests)
|
||||
* [Display content warning title above text](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
|
||||
* [Add notifications tab for posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/posts-notifications-tab)
|
||||
* [Show visibility of original post when replying](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/display-reply-visibility)
|
||||
* [Clickable reply/boost line above posts](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:clickable-boost-reply-line)
|
||||
* [Clickable reply line while replying to open original post](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/clickable-reply-line-compose)
|
||||
|
||||
|
||||
### Behavior
|
||||
|
||||
* Allow for confirmation before reblogging
|
||||
* Adding a bottom option for the publish button, allowing for easier use on larger screens!
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon: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:megalodon: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:megalodon: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:megalodon:spoiler-height-independent) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/166))
|
||||
* [Option to hide interaction numbers](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/hide-interaction-numbers)
|
||||
* [Option to always reveal content warnings](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/cw-above-text)
|
||||
* [Option to disable scrolling title bars](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:settings/disable-marquee)
|
||||
|
||||
|
||||
### Visual
|
||||
|
||||
* [Custom extended footer redesign](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:compact-extended-footer)
|
||||
* [Improvements to the true black mode](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:true-black-improvements)
|
||||
* [Profile header tweaks](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:ui/profile-header-tweaks)
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
As this app is using Java 17 features, you need JDK 17 or newer to build it. Other than that, everything is pretty standard. You can either import the project into Android Studio and build it from there, or run the following command in the project directory:
|
||||
|
||||
|
@ -97,13 +189,14 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
|
|||
|
||||
This project is released under the [GPL-3 License](./LICENSE).
|
||||
|
||||
## Contact & Support
|
||||
|
||||
**<a rel="me" href="https://floss.social/@moshidon">@moshidon@floss.social</a>**
|
||||
|
||||
[Official Matrix Chatroom](https://matrix.to/#/#moshidon:floss.social)
|
||||
## Links
|
||||
|
||||
[F.A.Q](FAQ.md)
|
||||
|
||||
[Moshidon Roadmap](https://github.com/users/LucasGGamerM/projects/1)
|
||||
[Official matrix chatroom:](https://matrix.to/#/#moshidon:floss.social) https://matrix.to/#/#moshidon:floss.social
|
||||
|
||||
[Moshidon roadmap](https://github.com/users/LucasGGamerM/projects/1)
|
||||
|
||||
<a rel="me" href="https://floss.social/@moshidon">@moshidon<wbr>@floss.social</a>
|
||||
|
||||
---
|
||||
|
|
24
build.gradle
24
build.gradle
|
@ -1,3 +1,23 @@
|
|||
plugins {
|
||||
id("com.android.application") version "8.7.2" apply false
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
content {
|
||||
includeModule 'com.github.UnifiedPush', 'android-connector'
|
||||
}
|
||||
}
|
||||
mavenLocal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.0.0'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
0
fix-metadata-markdown-lists.sh
Normal file → Executable file
0
fix-metadata-markdown-lists.sh
Normal file → Executable file
|
@ -17,5 +17,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=false
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=false
|
||||
org.gradle.configuration-cache=true
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,7 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=57dafb5c2622c6cc08b993c85b7c06956a2f53536432a30ead46166dbca0f1e9
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
||||
distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -15,20 +15,13 @@ android {
|
|||
archivesBaseName = "moshidon"
|
||||
applicationId "org.joinmastodon.android.moshinda"
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode 108
|
||||
versionName "2.3.0+fork.108.moshinda.luke"
|
||||
targetSdk 33
|
||||
versionCode 101
|
||||
versionName "1.2.3+fork.101.moshinda"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs.
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles.
|
||||
includeInBundle = false
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
nightly{
|
||||
storeFile = file("keystore/nightly_keystore.jks")
|
||||
|
@ -51,28 +44,6 @@ android {
|
|||
keyPassword = properties.getProperty('SIGNING_KEY_PASSWORD')
|
||||
}
|
||||
}
|
||||
|
||||
// release{
|
||||
// storeFile = file("keystore/release_keystore.jks")
|
||||
// storePassword System.getenv("RELEASE_SIGNING_STORE_PASSWORD")
|
||||
// if (storePassword == null) {
|
||||
// Properties properties = new Properties()
|
||||
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||
// storePassword = properties.getProperty('RELEASE_SIGNING_STORE_PASSWORD')
|
||||
// }
|
||||
// keyAlias System.getenv("RELEASE_SIGNING_KEY_ALIAS")
|
||||
// if (keyAlias == null) {
|
||||
// Properties properties = new Properties()
|
||||
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||
// keyAlias = properties.getProperty('RELEASE_SIGNING_KEY_ALIAS')
|
||||
// }
|
||||
// keyPassword System.getenv("RELEASE_SIGNING_KEY_PASSWORD")
|
||||
// if (keyPassword == null) {
|
||||
// Properties properties = new Properties()
|
||||
// properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||
// keyPassword = properties.getProperty('RELEASE_SIGNING_KEY_PASSWORD')
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -109,17 +80,9 @@ android {
|
|||
shrinkResources true
|
||||
versionNameSuffix '-play'
|
||||
}
|
||||
githubRelease {
|
||||
initWith release
|
||||
versionNameSuffix '-github'
|
||||
}
|
||||
fdroidRelease {
|
||||
initWith release
|
||||
vcsInfo.include false
|
||||
// The F-droid build system doesn't like this at all for some reason.
|
||||
// versionNameSuffix '-fdroid'
|
||||
// signingConfig signingConfigs.release
|
||||
}
|
||||
githubRelease { initWith release }
|
||||
playRelease { initWith release }
|
||||
fdroidRelease { initWith release }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
|
@ -155,7 +118,7 @@ dependencies {
|
|||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.16'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.9'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
@ -164,7 +127,7 @@ dependencies {
|
|||
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
|
||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
implementation 'org.unifiedpush.android:connector:3.0.7'
|
||||
implementation 'com.github.UnifiedPush:android-connector:2.1.1'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
|
|
2
mastodon/proguard-rules.pro
vendored
2
mastodon/proguard-rules.pro
vendored
|
@ -20,8 +20,6 @@
|
|||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontwarn android.app.BroadcastOptions
|
||||
|
||||
# Keep all model classes as they're used with gson and their names are shown in errors
|
||||
-keep public class org.joinmastodon.android.model.**{
|
||||
<fields>;
|
||||
|
|
|
@ -257,9 +257,5 @@ public class UiUtilsTest {
|
|||
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "-- * (asterisk) --")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("they/(she?)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "they/(she?)...")
|
||||
)).orElseThrow());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package org.joinmastodon.android.utils;
|
||||
|
||||
import static org.joinmastodon.android.model.FilterAction.*;
|
||||
import static org.joinmastodon.android.model.FilterContext.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
public class StatusFilterPredicateTest {
|
||||
|
||||
private static final LegacyFilter hideMeFilter = new LegacyFilter(), warnMeFilter = new LegacyFilter();
|
||||
private static final List<LegacyFilter> allFilters = List.of(hideMeFilter, warnMeFilter);
|
||||
|
||||
private static final Status
|
||||
hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()),
|
||||
warnInHomePublic = Status.ofFake(null, "display me with a warning", Instant.now());
|
||||
|
||||
static {
|
||||
hideMeFilter.phrase = "hide me";
|
||||
hideMeFilter.filterAction = HIDE;
|
||||
hideMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||
|
||||
warnMeFilter.phrase = "warning";
|
||||
warnMeFilter.filterAction = WARN;
|
||||
warnMeFilter.context = EnumSet.of(PUBLIC, HOME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHide() {
|
||||
assertFalse("should not pass because matching filter applies to given context",
|
||||
new StatusFilterPredicate(allFilters, HOME).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideRegardlessOfContext() {
|
||||
assertTrue("filters without context should always pass",
|
||||
new StatusFilterPredicate(allFilters, null).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideInDifferentContext() {
|
||||
assertTrue("should pass because matching filter does not apply to given context",
|
||||
new StatusFilterPredicate(allFilters, THREAD).test(hideInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHideWithWarningText() {
|
||||
assertTrue("should pass because matching filter is for warnings",
|
||||
new StatusFilterPredicate(allFilters, HOME).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarn() {
|
||||
assertFalse("should not pass because filter applies to given context",
|
||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnRegardlessOfContext() {
|
||||
assertTrue("filters without context should always pass",
|
||||
new StatusFilterPredicate(allFilters, null, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnInDifferentContext() {
|
||||
assertTrue("should pass because filter does not apply to given context",
|
||||
new StatusFilterPredicate(allFilters, THREAD, WARN).test(warnInHomePublic));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWarnWithHideText() {
|
||||
assertTrue("should pass because matching filter is for hiding",
|
||||
new StatusFilterPredicate(allFilters, HOME, WARN).test(hideInHomePublic));
|
||||
}
|
||||
}
|
|
@ -211,13 +211,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||
if(state==UpdateState.DOWNLOADING)
|
||||
throw new IllegalStateException();
|
||||
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED);
|
||||
}else{
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
}
|
||||
|
||||
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||
downloadID=dm.enqueue(
|
||||
new DownloadManager.Request(Uri.parse(getPrefs().getString("apkURL", null)))
|
||||
.setDestinationUri(Uri.fromFile(getUpdateApkFile()))
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||
|
@ -32,7 +31,6 @@
|
|||
android:name=".MastodonApp"
|
||||
android:allowBackup="true"
|
||||
android:label="@string/mo_app_name"
|
||||
android:dataExtractionRules="@xml/backup_rules"
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
@ -82,15 +80,6 @@
|
|||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".ChooseAccountForComposeActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/TransparentDialog">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CHOOSER"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
|
||||
|
||||
|
@ -116,11 +105,13 @@
|
|||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name="org.joinmastodon.android.utils.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:name=".TweakedFileProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths"/>
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
|
|
@ -1,48 +1,56 @@
|
|||
13bells.com
|
||||
1611.social
|
||||
4aem.com
|
||||
5dollah.click
|
||||
adachi.party
|
||||
adtension.com
|
||||
anime.website
|
||||
annihilation.social
|
||||
anon-kenkai.com
|
||||
asbestos.cafe
|
||||
bae.st
|
||||
bajax.us
|
||||
banepo.st
|
||||
baraag.net
|
||||
bassam.social
|
||||
battlepenguin.video
|
||||
beefyboys.win
|
||||
beepboop.ga
|
||||
berserker.town
|
||||
bikeshed.party
|
||||
boks.moe
|
||||
boymoder.biz
|
||||
brainsoap.net
|
||||
breastmilk.club
|
||||
brighteon.social
|
||||
cachapa.xyz
|
||||
canary.fedinuke.example.com
|
||||
catgirl.life
|
||||
bungle.online
|
||||
cawfee.club
|
||||
childlove.su
|
||||
clew.lol
|
||||
clubcyberia.co
|
||||
collapsitarian.io
|
||||
comfyboy.club
|
||||
contrapointsfan.club
|
||||
cottoncandy.cafe
|
||||
crlf.ninja
|
||||
crucible.world
|
||||
cum.camp
|
||||
cum.salon
|
||||
cunnyborea.space
|
||||
darknight-coffee.org
|
||||
decayable.ink
|
||||
dembased.xyz
|
||||
desupost.soy
|
||||
detroitriotcity.com
|
||||
djsumdog.com
|
||||
eatthebugs.social
|
||||
eientei.org
|
||||
elementality.org
|
||||
eveningzoo.club
|
||||
firedragonstudios.com
|
||||
firefaithfellowship.com
|
||||
fluf.club
|
||||
foxgirl.lol
|
||||
foxfam.club
|
||||
freak.university
|
||||
freeatlantis.com
|
||||
freedomstrike.org
|
||||
freesoftwareextremist.com
|
||||
freespeech.group
|
||||
freespeechextremist.com
|
||||
freetalklive.com
|
||||
froth.zone
|
||||
fsebugoutzone.org
|
||||
fulltermprivacy.com
|
||||
gameliberty.club
|
||||
gearlandia.haus
|
||||
genderheretics.xyz
|
||||
|
@ -51,58 +59,62 @@ gleasonator.com
|
|||
glee.li
|
||||
glindr.org
|
||||
goyim.app
|
||||
h5q.net
|
||||
goyslop.cafe
|
||||
haeder.net
|
||||
handholding.io
|
||||
harpy.faith
|
||||
hitchhiker.social
|
||||
hunk.city
|
||||
iddqd.social
|
||||
intkos.link
|
||||
justicewarrior.social
|
||||
kawa-kun.com
|
||||
kitsunemimi.club
|
||||
kiwifarms.cc
|
||||
kompost.cz
|
||||
kurosawa.moe
|
||||
kyaruc.moe
|
||||
leafposter.club
|
||||
leftychan.net
|
||||
lewdieheaven.com
|
||||
liberdon.com
|
||||
ligma.pro
|
||||
loli.church
|
||||
lolicon.rocks
|
||||
lolison.network
|
||||
lolison.top
|
||||
lovingexpressions.net
|
||||
mahodou.moe
|
||||
makemysarcophagus.com
|
||||
maladaptive.art
|
||||
marsey.moe
|
||||
masochi.st
|
||||
mastinator.com
|
||||
merovingian.club
|
||||
midwaytrades.com
|
||||
mirr0r.city
|
||||
morale.ch
|
||||
moa.st
|
||||
mouse.services
|
||||
mugicha.club
|
||||
narrativerry.xyz
|
||||
natehiggers.online
|
||||
nationalist.social
|
||||
neckbeard.xyz
|
||||
needs.vodka
|
||||
neenster.org
|
||||
nicecrew.digital
|
||||
nightshift.social
|
||||
nnia.space
|
||||
noagendasocial.com
|
||||
noagendasocial.nl
|
||||
noagendatube.com
|
||||
noauthority.social
|
||||
nobodyhasthe.biz
|
||||
norwoodzero.net
|
||||
nyanide.com
|
||||
nukem.biz
|
||||
obo.sh
|
||||
onionfarms.org
|
||||
parcero.bond
|
||||
pawlicker.com
|
||||
pawoo.net
|
||||
pedo.school
|
||||
peervideo.club
|
||||
piazza.today
|
||||
pibvt.net
|
||||
pieville.net
|
||||
pisskey.io
|
||||
plagu.ee
|
||||
pmth.us
|
||||
poa.st
|
||||
poast.org
|
||||
poast.tv
|
||||
|
@ -111,18 +123,17 @@ prospeech.space
|
|||
quodverum.com
|
||||
r18.social
|
||||
rakket.app
|
||||
rapemeat.express
|
||||
rapemeat.solutions
|
||||
rayci.st
|
||||
rdrama.cc
|
||||
rebelbase.site
|
||||
retardedniggers.forsale
|
||||
rojogato.com
|
||||
ryona.agency
|
||||
sad.cab
|
||||
schwartzwelt.xyz
|
||||
seal.cafe
|
||||
shaw.app
|
||||
shigusegubu.club
|
||||
shitpost.cloud
|
||||
shortstacksran.ch
|
||||
shota.house
|
||||
silliness.observer
|
||||
skinheads.eu
|
||||
skinheads.io
|
||||
|
@ -137,24 +148,23 @@ sneed.social
|
|||
sonichu.com
|
||||
spinster.xyz
|
||||
springbo.cc
|
||||
starnix.network
|
||||
strelizia.net
|
||||
taihou.website
|
||||
syspxl.xyz
|
||||
tastingtraffic.net
|
||||
teci.world
|
||||
theapex.social
|
||||
theblab.org
|
||||
thechimp.zone
|
||||
thenobody.club
|
||||
thepostearthdestination.com
|
||||
tkammer.de
|
||||
trumpislovetrumpis.life
|
||||
truthsocial.co.in
|
||||
usualsuspects.lol
|
||||
vampiremaid.cafe
|
||||
urchan.org
|
||||
varishangout.net
|
||||
vtuberfan.social
|
||||
whinge.house
|
||||
whinge.town
|
||||
wideboys.org
|
||||
wolfgirl.bar
|
||||
xn--p1abe3d.xn--80asehdb
|
||||
yggdrasil.social
|
||||
youjo.love
|
||||
zhub.link
|
||||
zztails.gay
|
||||
|
|
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
File diff suppressed because it is too large
Load diff
|
@ -88,13 +88,8 @@ public class AudioPlayerService extends Service{
|
|||
nm=getSystemService(NotificationManager.class);
|
||||
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
|
||||
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE), RECEIVER_EXPORTED);
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_STOP), RECEIVER_EXPORTED);
|
||||
}else{
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
|
||||
}
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
|
||||
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
|
||||
instance=this;
|
||||
}
|
||||
|
||||
|
@ -271,7 +266,7 @@ public class AudioPlayerService extends Service{
|
|||
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||
Notification.Builder bldr=new Notification.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_ntf_logo)
|
||||
.setContentTitle(status.account.getDisplayName())
|
||||
.setContentTitle(status.account.displayName)
|
||||
.setContentText(HtmlParser.strip(status.content))
|
||||
.setOngoing(!dismissable)
|
||||
.setShowWhen(false)
|
||||
|
@ -286,7 +281,7 @@ public class AudioPlayerService extends Service{
|
|||
|
||||
if(playerReady){
|
||||
boolean isPlaying=player.isPlaying();
|
||||
bldr.addAction(new Notification.Action.Builder(Icon.createWithResource(this, isPlaying ? R.drawable.ic_fluent_pause_24_filled : R.drawable.ic_fluent_play_24_filled),
|
||||
bldr.addAction(new Notification.Action.Builder(Icon.createWithResource(this, isPlaying ? R.drawable.ic_pause_24 : R.drawable.ic_play_24),
|
||||
getString(isPlaying ? R.string.pause : R.string.play),
|
||||
PendingIntent.getBroadcast(this, 2, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_IMMUTABLE))
|
||||
.build());
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
package org.joinmastodon.android;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
|
||||
public class ChooseAccountForComposeActivity extends FragmentStackActivity{
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState == null && Objects.equals(getIntent().getAction(), Intent.ACTION_CHOOSER)) {
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||
if (sessions.isEmpty()){
|
||||
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
} else if (sessions.size() > 1) {
|
||||
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, R.drawable.ic_fluent_compose_28_regular,
|
||||
R.string.choose_account, null, false);
|
||||
sheet.setOnClick((accountId, open) -> {
|
||||
openComposeFragment(accountId);
|
||||
});
|
||||
sheet.show();
|
||||
} else if (sessions.size() == 1) {
|
||||
openComposeFragment(sessions.get(0).getID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openComposeFragment(String accountID){
|
||||
getWindow().setBackgroundDrawable(null);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Fragment fragment=new ComposeFragment();
|
||||
fragment.setArguments(args);
|
||||
showFragmentClearingBackStack(fragment);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.jsoup.internal.StringUtil;
|
||||
|
||||
|
@ -32,9 +32,10 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
if(savedInstanceState==null){
|
||||
|
||||
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
|
||||
Optional<Pair<String, Optional<String>>> fediHandle = text.flatMap(UiUtils::parseFediverseHandle);
|
||||
boolean isFediUrl = text.map(UiUtils::looksLikeFediverseUrl).orElse(false);
|
||||
boolean isFediUrl = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
|
||||
boolean isOpenable = isFediUrl || fediHandle.isPresent();
|
||||
|
||||
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
|
||||
|
@ -42,11 +43,7 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
} else if (isOpenable || sessions.size() > 1) {
|
||||
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, R.drawable.ic_fluent_share_28_regular,
|
||||
isOpenable
|
||||
? R.string.sk_external_share_or_open_title
|
||||
: R.string.sk_external_share_title,
|
||||
null, isOpenable);
|
||||
AccountSwitcherSheet sheet = new AccountSwitcherSheet(this, null, true, isOpenable);
|
||||
sheet.setOnClick((accountId, open) -> {
|
||||
if (open && text.isPresent()) {
|
||||
BiConsumer<Class<? extends Fragment>, Bundle> callback = (clazz, args) -> {
|
||||
|
@ -86,8 +83,6 @@ public class ExternalShareActivity extends FragmentStackActivity{
|
|||
}
|
||||
|
||||
private void openComposeFragment(String accountID){
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
UiUtils.setUserPreferredTheme(this, session);
|
||||
getWindow().setBackgroundDrawable(null);
|
||||
|
||||
Intent intent=getIntent();
|
||||
|
|
|
@ -1,841 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.joinmastodon.android;
|
||||
|
||||
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.START_TAG;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing
|
||||
* of files associated with an app by creating a <code>content://</code> {@link Uri} for a file
|
||||
* instead of a <code>file:///</code> {@link Uri}.
|
||||
* <p>
|
||||
* A content URI allows you to grant read and write access using
|
||||
* temporary access permissions. When you create an {@link Intent} containing
|
||||
* a content URI, in order to send the content URI
|
||||
* to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add
|
||||
* permissions. These permissions are available to the client app for as long as the stack for
|
||||
* a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a
|
||||
* {@link android.app.Service}, the permissions are available as long as the
|
||||
* {@link android.app.Service} is running.
|
||||
* <p>
|
||||
* In comparison, to control access to a <code>file:///</code> {@link Uri} you have to modify the
|
||||
* file system permissions of the underlying file. The permissions you provide become available to
|
||||
* <em>any</em> app, and remain in effect until you change them. This level of access is
|
||||
* fundamentally insecure.
|
||||
* <p>
|
||||
* The increased level of file access security offered by a content URI
|
||||
* makes FileProvider a key part of Android's security infrastructure.
|
||||
* <p>
|
||||
* This overview of FileProvider includes the following topics:
|
||||
* </p>
|
||||
* <ol>
|
||||
* <li><a href="#ProviderDefinition">Defining a FileProvider</a></li>
|
||||
* <li><a href="#SpecifyFiles">Specifying Available Files</a></li>
|
||||
* <li><a href="#GetUri">Retrieving the Content URI for a File</li>
|
||||
* <li><a href="#Permissions">Granting Temporary Permissions to a URI</a></li>
|
||||
* <li><a href="#ServeUri">Serving a Content URI to Another App</a></li>
|
||||
* </ol>
|
||||
* <h3 id="ProviderDefinition">Defining a FileProvider</h3>
|
||||
* <p>
|
||||
* Since the default functionality of FileProvider includes content URI generation for files, you
|
||||
* don't need to define a subclass in code. Instead, you can include a FileProvider in your app
|
||||
* by specifying it entirely in XML. To specify the FileProvider component itself, add a
|
||||
* <code><a href="{@docRoot}guide/topics/manifest/provider-element.html"><provider></a></code>
|
||||
* element to your app manifest. Set the <code>android:name</code> attribute to
|
||||
* <code>androidx.core.content.FileProvider</code>. Set the <code>android:authorities</code>
|
||||
* attribute to a URI authority based on a domain you control; for example, if you control the
|
||||
* domain <code>mydomain.com</code> you should use the authority
|
||||
* <code>com.mydomain.fileprovider</code>. Set the <code>android:exported</code> attribute to
|
||||
* <code>false</code>; the FileProvider does not need to be public. Set the
|
||||
* <a href="{@docRoot}guide/topics/manifest/provider-element.html#gprmsn"
|
||||
* >android:grantUriPermissions</a> attribute to <code>true</code>, to allow you
|
||||
* to grant temporary access to files. For example:
|
||||
* <pre class="prettyprint">
|
||||
*<manifest>
|
||||
* ...
|
||||
* <application>
|
||||
* ...
|
||||
* <provider
|
||||
* android:name="androidx.core.content.FileProvider"
|
||||
* android:authorities="com.mydomain.fileprovider"
|
||||
* android:exported="false"
|
||||
* android:grantUriPermissions="true">
|
||||
* ...
|
||||
* </provider>
|
||||
* ...
|
||||
* </application>
|
||||
*</manifest></pre>
|
||||
* <p>
|
||||
* If you want to override any of the default behavior of FileProvider methods, extend
|
||||
* the FileProvider class and use the fully-qualified class name in the <code>android:name</code>
|
||||
* attribute of the <code><provider></code> element.
|
||||
* <h3 id="SpecifyFiles">Specifying Available Files</h3>
|
||||
* A FileProvider can only generate a content URI for files in directories that you specify
|
||||
* beforehand. To specify a directory, specify the its storage area and path in XML, using child
|
||||
* elements of the <code><paths></code> element.
|
||||
* For example, the following <code>paths</code> element tells FileProvider that you intend to
|
||||
* request content URIs for the <code>images/</code> subdirectory of your private file area.
|
||||
* <pre class="prettyprint">
|
||||
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
* <files-path name="my_images" path="images/"/>
|
||||
* ...
|
||||
*</paths>
|
||||
*</pre>
|
||||
* <p>
|
||||
* The <code><paths></code> element must contain one or more of the following child elements:
|
||||
* </p>
|
||||
* <dl>
|
||||
* <dt>
|
||||
* <pre class="prettyprint">
|
||||
*<files-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</pre>
|
||||
* </dt>
|
||||
* <dd>
|
||||
* Represents files in the <code>files/</code> subdirectory of your app's internal storage
|
||||
* area. This subdirectory is the same as the value returned by {@link Context#getFilesDir()
|
||||
* Context.getFilesDir()}.
|
||||
* </dd>
|
||||
* <dt>
|
||||
* <pre>
|
||||
*<cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</pre>
|
||||
* <dt>
|
||||
* <dd>
|
||||
* Represents files in the cache subdirectory of your app's internal storage area. The root path
|
||||
* of this subdirectory is the same as the value returned by {@link Context#getCacheDir()
|
||||
* getCacheDir()}.
|
||||
* </dd>
|
||||
* <dt>
|
||||
* <pre class="prettyprint">
|
||||
*<external-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</pre>
|
||||
* </dt>
|
||||
* <dd>
|
||||
* Represents files in the root of the external storage area. The root path of this subdirectory
|
||||
* is the same as the value returned by
|
||||
* {@link Environment#getExternalStorageDirectory() Environment.getExternalStorageDirectory()}.
|
||||
* </dd>
|
||||
* <dt>
|
||||
* <pre class="prettyprint">
|
||||
*<external-files-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</pre>
|
||||
* </dt>
|
||||
* <dd>
|
||||
* Represents files in the root of your app's external storage area. The root path of this
|
||||
* subdirectory is the same as the value returned by
|
||||
* {@code Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)}.
|
||||
* </dd>
|
||||
* <dt>
|
||||
* <pre class="prettyprint">
|
||||
*<external-cache-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</pre>
|
||||
* </dt>
|
||||
* <dd>
|
||||
* Represents files in the root of your app's external cache area. The root path of this
|
||||
* subdirectory is the same as the value returned by
|
||||
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
|
||||
* </dd>
|
||||
* <dt>
|
||||
* <pre class="prettyprint">
|
||||
*<external-media-path name="<i>name</i>" path="<i>path</i>" />
|
||||
*</pre>
|
||||
* </dt>
|
||||
* <dd>
|
||||
* Represents files in the root of your app's external media area. The root path of this
|
||||
* subdirectory is the same as the value returned by the first result of
|
||||
* {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
|
||||
* <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
|
||||
* </dd>
|
||||
* </dl>
|
||||
* <p>
|
||||
* These child elements all use the same attributes:
|
||||
* </p>
|
||||
* <dl>
|
||||
* <dt>
|
||||
* <code>name="<i>name</i>"</code>
|
||||
* </dt>
|
||||
* <dd>
|
||||
* A URI path segment. To enforce security, this value hides the name of the subdirectory
|
||||
* you're sharing. The subdirectory name for this value is contained in the
|
||||
* <code>path</code> attribute.
|
||||
* </dd>
|
||||
* <dt>
|
||||
* <code>path="<i>path</i>"</code>
|
||||
* </dt>
|
||||
* <dd>
|
||||
* The subdirectory you're sharing. While the <code>name</code> attribute is a URI path
|
||||
* segment, the <code>path</code> value is an actual subdirectory name. Notice that the
|
||||
* value refers to a <b>subdirectory</b>, not an individual file or files. You can't
|
||||
* share a single file by its file name, nor can you specify a subset of files using
|
||||
* wildcards.
|
||||
* </dd>
|
||||
* </dl>
|
||||
* <p>
|
||||
* You must specify a child element of <code><paths></code> for each directory that contains
|
||||
* files for which you want content URIs. For example, these XML elements specify two directories:
|
||||
* <pre class="prettyprint">
|
||||
*<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
* <files-path name="my_images" path="images/"/>
|
||||
* <files-path name="my_docs" path="docs/"/>
|
||||
*</paths>
|
||||
*</pre>
|
||||
* <p>
|
||||
* Put the <code><paths></code> element and its children in an XML file in your project.
|
||||
* For example, you can add them to a new file called <code>res/xml/file_paths.xml</code>.
|
||||
* To link this file to the FileProvider, add a
|
||||
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html"><meta-data></a> element
|
||||
* as a child of the <code><provider></code> element that defines the FileProvider. Set the
|
||||
* <code><meta-data></code> element's "android:name" attribute to
|
||||
* <code>android.support.FILE_PROVIDER_PATHS</code>. Set the element's "android:resource" attribute
|
||||
* to <code>@xml/file_paths</code> (notice that you don't specify the <code>.xml</code>
|
||||
* extension). For example:
|
||||
* <pre class="prettyprint">
|
||||
*<provider
|
||||
* android:name="androidx.core.content.FileProvider"
|
||||
* android:authorities="com.mydomain.fileprovider"
|
||||
* android:exported="false"
|
||||
* android:grantUriPermissions="true">
|
||||
* <meta-data
|
||||
* android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
* android:resource="@xml/file_paths" />
|
||||
*</provider>
|
||||
*</pre>
|
||||
* <h3 id="GetUri">Generating the Content URI for a File</h3>
|
||||
* <p>
|
||||
* To share a file with another app using a content URI, your app has to generate the content URI.
|
||||
* To generate the content URI, create a new {@link File} for the file, then pass the {@link File}
|
||||
* to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI
|
||||
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an
|
||||
* {@link Intent}. The client app that receives the content URI can open the file
|
||||
* and access its contents by calling
|
||||
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
|
||||
* ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}.
|
||||
* <p>
|
||||
* For example, suppose your app is offering files to other apps with a FileProvider that has the
|
||||
* authority <code>com.mydomain.fileprovider</code>. To get a content URI for the file
|
||||
* <code>default_image.jpg</code> in the <code>images/</code> subdirectory of your internal storage
|
||||
* add the following code:
|
||||
* <pre class="prettyprint">
|
||||
*File imagePath = new File(Context.getFilesDir(), "images");
|
||||
*File newFile = new File(imagePath, "default_image.jpg");
|
||||
*Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
|
||||
*</pre>
|
||||
* As a result of the previous snippet,
|
||||
* {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI
|
||||
* <code>content://com.mydomain.fileprovider/my_images/default_image.jpg</code>.
|
||||
* <h3 id="Permissions">Granting Temporary Permissions to a URI</h3>
|
||||
* To grant an access permission to a content URI returned from
|
||||
* {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following:
|
||||
* <ul>
|
||||
* <li>
|
||||
* Call the method
|
||||
* {@link Context#grantUriPermission(String, Uri, int)
|
||||
* Context.grantUriPermission(package, Uri, mode_flags)} for the <code>content://</code>
|
||||
* {@link Uri}, using the desired mode flags. This grants temporary access permission for the
|
||||
* content URI to the specified package, according to the value of the
|
||||
* the <code>mode_flags</code> parameter, which you can set to
|
||||
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}
|
||||
* or both. The permission remains in effect until you revoke it by calling
|
||||
* {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device
|
||||
* reboots.
|
||||
* </li>
|
||||
* <li>
|
||||
* Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}.
|
||||
* </li>
|
||||
* <li>
|
||||
* Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either
|
||||
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or
|
||||
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both.
|
||||
* </li>
|
||||
* <li>
|
||||
* Finally, send the {@link Intent} to
|
||||
* another app. Most often, you do this by calling
|
||||
* {@link android.app.Activity#setResult(int, Intent) setResult()}.
|
||||
* <p>
|
||||
* Permissions granted in an {@link Intent} remain in effect while the stack of the receiving
|
||||
* {@link android.app.Activity} is active. When the stack finishes, the permissions are
|
||||
* automatically removed. Permissions granted to one {@link android.app.Activity} in a client
|
||||
* app are automatically extended to other components of that app.
|
||||
* </p>
|
||||
* </li>
|
||||
* </ul>
|
||||
* <h3 id="ServeUri">Serving a Content URI to Another App</h3>
|
||||
* <p>
|
||||
* There are a variety of ways to serve the content URI for a file to a client app. One common way
|
||||
* is for the client app to start your app by calling
|
||||
* {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()},
|
||||
* which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app.
|
||||
* In response, your app can immediately return a content URI to the client app or present a user
|
||||
* interface that allows the user to pick a file. In the latter case, once the user picks the file
|
||||
* your app can return its content URI. In both cases, your app returns the content URI in an
|
||||
* {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}.
|
||||
* </p>
|
||||
* <p>
|
||||
* You can also put the content URI in a {@link android.content.ClipData} object and then add the
|
||||
* object to an {@link Intent} you send to a client app. To do this, call
|
||||
* {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can
|
||||
* add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own
|
||||
* content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent}
|
||||
* to set temporary access permissions, the same permissions are applied to all of the content
|
||||
* URIs.
|
||||
* </p>
|
||||
* <p class="note">
|
||||
* <strong>Note:</strong> The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is
|
||||
* only available in platform version 16 (Android 4.1) and later. If you want to maintain
|
||||
* compatibility with previous versions, you should send one content URI at a time in the
|
||||
* {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling
|
||||
* {@link Intent#setData setData()}.
|
||||
* </p>
|
||||
* <h3 id="">More Information</h3>
|
||||
* <p>
|
||||
* To learn more about FileProvider, see the Android training class
|
||||
* <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files Securely with URIs</a>.
|
||||
* </p>
|
||||
*/
|
||||
public class FileProvider extends ContentProvider {
|
||||
private static final String[] COLUMNS = {
|
||||
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
||||
|
||||
private static final String
|
||||
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
|
||||
|
||||
private static final String TAG_ROOT_PATH = "root-path";
|
||||
private static final String TAG_FILES_PATH = "files-path";
|
||||
private static final String TAG_CACHE_PATH = "cache-path";
|
||||
private static final String TAG_EXTERNAL = "external-path";
|
||||
private static final String TAG_EXTERNAL_FILES = "external-files-path";
|
||||
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
|
||||
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
|
||||
|
||||
private static final String ATTR_NAME = "name";
|
||||
private static final String ATTR_PATH = "path";
|
||||
|
||||
private static final File DEVICE_ROOT = new File("/");
|
||||
|
||||
@GuardedBy("sCache")
|
||||
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
|
||||
|
||||
private PathStrategy mStrategy;
|
||||
|
||||
/**
|
||||
* The default FileProvider implementation does not need to be initialized. If you want to
|
||||
* override this method, you must provide your own subclass of FileProvider.
|
||||
*/
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* After the FileProvider is instantiated, this method is called to provide the system with
|
||||
* information about the provider.
|
||||
*
|
||||
* @param context A {@link Context} for the current component.
|
||||
* @param info A {@link ProviderInfo} for the new provider.
|
||||
*/
|
||||
@Override
|
||||
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
|
||||
super.attachInfo(context, info);
|
||||
|
||||
// Sanity check our security
|
||||
if (info.exported) {
|
||||
throw new SecurityException("Provider must not be exported");
|
||||
}
|
||||
if (!info.grantUriPermissions) {
|
||||
throw new SecurityException("Provider must grant uri permissions");
|
||||
}
|
||||
|
||||
mStrategy = getPathStrategy(context, info.authority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a content URI for a given {@link File}. Specific temporary
|
||||
* permissions for the content URI can be set with
|
||||
* {@link Context#grantUriPermission(String, Uri, int)}, or added
|
||||
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
|
||||
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
|
||||
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
|
||||
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
|
||||
* <code>content</code> {@link Uri} for file paths defined in their <code><paths></code>
|
||||
* meta-data element. See the Class Overview for more information.
|
||||
*
|
||||
* @param context A {@link Context} for the current component.
|
||||
* @param authority The authority of a {@link FileProvider} defined in a
|
||||
* {@code <provider>} element in your app's manifest.
|
||||
* @param file A {@link File} pointing to the filename for which you want a
|
||||
* <code>content</code> {@link Uri}.
|
||||
* @return A content URI for the file.
|
||||
* @throws IllegalArgumentException When the given {@link File} is outside
|
||||
* the paths supported by the provider.
|
||||
*/
|
||||
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
|
||||
@NonNull File file) {
|
||||
final PathStrategy strategy = getPathStrategy(context, authority);
|
||||
return strategy.getUriForFile(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a content URI returned by
|
||||
* {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
|
||||
* managed by the FileProvider.
|
||||
* FileProvider reports the column names defined in {@link OpenableColumns}:
|
||||
* <ul>
|
||||
* <li>{@link OpenableColumns#DISPLAY_NAME}</li>
|
||||
* <li>{@link OpenableColumns#SIZE}</li>
|
||||
* </ul>
|
||||
* For more information, see
|
||||
* {@link ContentProvider#query(Uri, String[], String, String[], String)
|
||||
* ContentProvider.query()}.
|
||||
*
|
||||
* @param uri A content URI returned by {@link #getUriForFile}.
|
||||
* @param projection The list of columns to put into the {@link Cursor}. If null all columns are
|
||||
* included.
|
||||
* @param selection Selection criteria to apply. If null then all data that matches the content
|
||||
* URI is returned.
|
||||
* @param selectionArgs An array of {@link String}, containing arguments to bind to
|
||||
* the <i>selection</i> parameter. The <i>query</i> method scans <i>selection</i> from left to
|
||||
* right and iterates through <i>selectionArgs</i>, replacing the current "?" character in
|
||||
* <i>selection</i> with the value at the current position in <i>selectionArgs</i>. The
|
||||
* values are bound to <i>selection</i> as {@link String} values.
|
||||
* @param sortOrder A {@link String} containing the column name(s) on which to sort
|
||||
* the resulting {@link Cursor}.
|
||||
* @return A {@link Cursor} containing the results of the query.
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
|
||||
@Nullable String[] selectionArgs,
|
||||
@Nullable String sortOrder) {
|
||||
// ContentProvider has already checked granted permissions
|
||||
final File file = mStrategy.getFileForUri(uri);
|
||||
|
||||
if (projection == null) {
|
||||
projection = COLUMNS;
|
||||
}
|
||||
|
||||
String[] cols = new String[projection.length];
|
||||
Object[] values = new Object[projection.length];
|
||||
int i = 0;
|
||||
for (String col : projection) {
|
||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||
cols[i] = OpenableColumns.DISPLAY_NAME;
|
||||
values[i++] = file.getName();
|
||||
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||
cols[i] = OpenableColumns.SIZE;
|
||||
values[i++] = file.length();
|
||||
}
|
||||
}
|
||||
|
||||
cols = copyOf(cols, i);
|
||||
values = copyOf(values, i);
|
||||
|
||||
final MatrixCursor cursor = new MatrixCursor(cols, 1);
|
||||
cursor.addRow(values);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MIME type of a content URI returned by
|
||||
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||
*
|
||||
* @param uri A content URI returned by
|
||||
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||
* @return If the associated file has an extension, the MIME type associated with that
|
||||
* extension; otherwise <code>application/octet-stream</code>.
|
||||
*/
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
// ContentProvider has already checked granted permissions
|
||||
final File file = mStrategy.getFileForUri(uri);
|
||||
|
||||
final int lastDot = file.getName().lastIndexOf('.');
|
||||
if (lastDot >= 0) {
|
||||
final String extension = file.getName().substring(lastDot + 1);
|
||||
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mime != null) {
|
||||
return mime;
|
||||
}
|
||||
}
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, this method throws an {@link UnsupportedOperationException}. You must
|
||||
* subclass FileProvider if you want to provide different functionality.
|
||||
*/
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException("No external inserts");
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, this method throws an {@link UnsupportedOperationException}. You must
|
||||
* subclass FileProvider if you want to provide different functionality.
|
||||
*/
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
|
||||
@Nullable String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("No external updates");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file associated with the specified content URI, as
|
||||
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
|
||||
* method does <b>not</b> throw an {@link IOException}; you must check its return value.
|
||||
*
|
||||
* @param uri A content URI for a file, as returned by
|
||||
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||
* @param selection Ignored. Set to {@code null}.
|
||||
* @param selectionArgs Ignored. Set to {@code null}.
|
||||
* @return 1 if the delete succeeds; otherwise, 0.
|
||||
*/
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, @Nullable String selection,
|
||||
@Nullable String[] selectionArgs) {
|
||||
// ContentProvider has already checked granted permissions
|
||||
final File file = mStrategy.getFileForUri(uri);
|
||||
return file.delete() ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, FileProvider automatically returns the
|
||||
* {@link ParcelFileDescriptor} for a file associated with a <code>content://</code>
|
||||
* {@link Uri}. To get the {@link ParcelFileDescriptor}, call
|
||||
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
|
||||
* ContentResolver.openFileDescriptor}.
|
||||
*
|
||||
* To override this method, you must provide your own subclass of FileProvider.
|
||||
*
|
||||
* @param uri A content URI associated with a file, as returned by
|
||||
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
|
||||
* @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
|
||||
* write access, or "rwt" for read and write access that truncates any existing file.
|
||||
* @return A new {@link ParcelFileDescriptor} with which you can access the file.
|
||||
*/
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
|
||||
throws FileNotFoundException {
|
||||
// ContentProvider has already checked granted permissions
|
||||
final File file = mStrategy.getFileForUri(uri);
|
||||
final int fileMode = modeToMode(mode);
|
||||
return ParcelFileDescriptor.open(file, fileMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@link PathStrategy} for given authority, either by parsing or
|
||||
* returning from cache.
|
||||
*/
|
||||
private static PathStrategy getPathStrategy(Context context, String authority) {
|
||||
PathStrategy strat;
|
||||
synchronized (sCache) {
|
||||
strat = sCache.get(authority);
|
||||
if (strat == null) {
|
||||
try {
|
||||
strat = parsePathStrategy(context, authority);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
|
||||
}
|
||||
sCache.put(authority, strat);
|
||||
}
|
||||
}
|
||||
return strat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and return {@link PathStrategy} for given authority as defined in
|
||||
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
|
||||
*
|
||||
* @see #getPathStrategy(Context, String)
|
||||
*/
|
||||
private static PathStrategy parsePathStrategy(Context context, String authority)
|
||||
throws IOException, XmlPullParserException {
|
||||
final SimplePathStrategy strat = new SimplePathStrategy(authority);
|
||||
|
||||
final ProviderInfo info = context.getPackageManager()
|
||||
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
|
||||
if (info == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Couldn't find meta-data for provider with authority " + authority);
|
||||
}
|
||||
|
||||
final XmlResourceParser in = info.loadXmlMetaData(
|
||||
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
|
||||
if (in == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
|
||||
}
|
||||
|
||||
int type;
|
||||
while ((type = in.next()) != END_DOCUMENT) {
|
||||
if (type == START_TAG) {
|
||||
final String tag = in.getName();
|
||||
|
||||
final String name = in.getAttributeValue(null, ATTR_NAME);
|
||||
String path = in.getAttributeValue(null, ATTR_PATH);
|
||||
|
||||
File target = null;
|
||||
if (TAG_ROOT_PATH.equals(tag)) {
|
||||
target = DEVICE_ROOT;
|
||||
} else if (TAG_FILES_PATH.equals(tag)) {
|
||||
target = context.getFilesDir();
|
||||
} else if (TAG_CACHE_PATH.equals(tag)) {
|
||||
target = context.getCacheDir();
|
||||
} else if (TAG_EXTERNAL.equals(tag)) {
|
||||
target = Environment.getExternalStorageDirectory();
|
||||
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
|
||||
File[] externalFilesDirs = context.getExternalFilesDirs(null);
|
||||
if (externalFilesDirs.length > 0) {
|
||||
target = externalFilesDirs[0];
|
||||
}
|
||||
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
|
||||
File[] externalCacheDirs = context.getExternalCacheDirs();
|
||||
if (externalCacheDirs.length > 0) {
|
||||
target = externalCacheDirs[0];
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
||||
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
|
||||
File[] externalMediaDirs = context.getExternalMediaDirs();
|
||||
if (externalMediaDirs.length > 0) {
|
||||
target = externalMediaDirs[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (target != null) {
|
||||
strat.addRoot(name, buildPath(target, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy for mapping between {@link File} and {@link Uri}.
|
||||
* <p>
|
||||
* Strategies must be symmetric so that mapping a {@link File} to a
|
||||
* {@link Uri} and then back to a {@link File} points at the original
|
||||
* target.
|
||||
* <p>
|
||||
* Strategies must remain consistent across app launches, and not rely on
|
||||
* dynamic state. This ensures that any generated {@link Uri} can still be
|
||||
* resolved if your process is killed and later restarted.
|
||||
*
|
||||
* @see SimplePathStrategy
|
||||
*/
|
||||
interface PathStrategy {
|
||||
/**
|
||||
* Return a {@link Uri} that represents the given {@link File}.
|
||||
*/
|
||||
Uri getUriForFile(File file);
|
||||
|
||||
/**
|
||||
* Return a {@link File} that represents the given {@link Uri}.
|
||||
*/
|
||||
File getFileForUri(Uri uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy that provides access to files living under a narrow whitelist of
|
||||
* filesystem roots. It will throw {@link SecurityException} if callers try
|
||||
* accessing files outside the configured roots.
|
||||
* <p>
|
||||
* For example, if configured with
|
||||
* {@code addRoot("myfiles", context.getFilesDir())}, then
|
||||
* {@code context.getFileStreamPath("foo.txt")} would map to
|
||||
* {@code content://myauthority/myfiles/foo.txt}.
|
||||
*/
|
||||
static class SimplePathStrategy implements PathStrategy {
|
||||
private final String mAuthority;
|
||||
private final HashMap<String, File> mRoots = new HashMap<String, File>();
|
||||
|
||||
SimplePathStrategy(String authority) {
|
||||
mAuthority = authority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mapping from a name to a filesystem root. The provider only offers
|
||||
* access to files that live under configured roots.
|
||||
*/
|
||||
void addRoot(String name, File root) {
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
throw new IllegalArgumentException("Name must not be empty");
|
||||
}
|
||||
|
||||
try {
|
||||
// Resolve to canonical path to keep path checking fast
|
||||
root = root.getCanonicalFile();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to resolve canonical path for " + root, e);
|
||||
}
|
||||
|
||||
mRoots.put(name, root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getUriForFile(File file) {
|
||||
String path;
|
||||
try {
|
||||
path = file.getCanonicalPath();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||
}
|
||||
|
||||
// Find the most-specific root path
|
||||
Map.Entry<String, File> mostSpecific = null;
|
||||
for (Map.Entry<String, File> root : mRoots.entrySet()) {
|
||||
final String rootPath = root.getValue().getPath();
|
||||
if (path.startsWith(rootPath) && (mostSpecific == null
|
||||
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
|
||||
mostSpecific = root;
|
||||
}
|
||||
}
|
||||
|
||||
if (mostSpecific == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to find configured root that contains " + path);
|
||||
}
|
||||
|
||||
// Start at first char of path under root
|
||||
final String rootPath = mostSpecific.getValue().getPath();
|
||||
if (rootPath.endsWith("/")) {
|
||||
path = path.substring(rootPath.length());
|
||||
} else {
|
||||
path = path.substring(rootPath.length() + 1);
|
||||
}
|
||||
|
||||
// Encode the tag and path separately
|
||||
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
|
||||
return new Uri.Builder().scheme("content")
|
||||
.authority(mAuthority).encodedPath(path).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFileForUri(Uri uri) {
|
||||
String path = uri.getEncodedPath();
|
||||
|
||||
final int splitIndex = path.indexOf('/', 1);
|
||||
final String tag = Uri.decode(path.substring(1, splitIndex));
|
||||
path = Uri.decode(path.substring(splitIndex + 1));
|
||||
|
||||
final File root = mRoots.get(tag);
|
||||
if (root == null) {
|
||||
throw new IllegalArgumentException("Unable to find configured root for " + uri);
|
||||
}
|
||||
|
||||
File file = new File(root, path);
|
||||
try {
|
||||
file = file.getCanonicalFile();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
|
||||
}
|
||||
|
||||
if (!file.getPath().startsWith(root.getPath())) {
|
||||
throw new SecurityException("Resolved path jumped beyond configured root");
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from ContentResolver.java
|
||||
*/
|
||||
private static int modeToMode(String mode) {
|
||||
int modeBits;
|
||||
if ("r".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||
| ParcelFileDescriptor.MODE_CREATE
|
||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
} else if ("wa".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
|
||||
| ParcelFileDescriptor.MODE_CREATE
|
||||
| ParcelFileDescriptor.MODE_APPEND;
|
||||
} else if ("rw".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||
| ParcelFileDescriptor.MODE_CREATE;
|
||||
} else if ("rwt".equals(mode)) {
|
||||
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
|
||||
| ParcelFileDescriptor.MODE_CREATE
|
||||
| ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid mode: " + mode);
|
||||
}
|
||||
return modeBits;
|
||||
}
|
||||
|
||||
private static File buildPath(File base, String... segments) {
|
||||
File cur = base;
|
||||
for (String segment : segments) {
|
||||
if (segment != null) {
|
||||
cur = new File(cur, segment);
|
||||
}
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
private static String[] copyOf(String[] original, int newLength) {
|
||||
final String[] result = new String[newLength];
|
||||
System.arraycopy(original, 0, result, 0, newLength);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Object[] copyOf(Object[] original, int newLength) {
|
||||
final Object[] result = new Object[newLength];
|
||||
System.arraycopy(original, 0, result, 0, newLength);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package org.joinmastodon.android;
|
||||
|
||||
import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
import static org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference.MATERIAL3;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -14,12 +13,12 @@ import com.google.gson.JsonSyntaxException;
|
|||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences.ColorPreference;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.utils.ColorPalette;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
|
@ -28,9 +27,6 @@ import java.util.HashSet;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GlobalUserPreferences{
|
||||
private static final String TAG="GlobalUserPreferences";
|
||||
|
||||
|
@ -45,6 +41,7 @@ public class GlobalUserPreferences{
|
|||
public static boolean showNewPostsButton;
|
||||
public static boolean toolbarMarquee;
|
||||
public static boolean disableSwipe;
|
||||
public static boolean voteButtonForSingleChoice;
|
||||
public static boolean enableDeleteNotifications;
|
||||
public static boolean translateButtonOpenedOnly;
|
||||
public static boolean uniformNotificationIcon;
|
||||
|
@ -56,42 +53,32 @@ public class GlobalUserPreferences{
|
|||
public static boolean collapseLongPosts;
|
||||
public static boolean spectatorMode;
|
||||
public static boolean autoHideFab;
|
||||
public static boolean compactReblogReplyLine;
|
||||
public static boolean allowRemoteLoading;
|
||||
public static boolean forwardReportDefault;
|
||||
public static AutoRevealMode autoRevealEqualSpoilers;
|
||||
public static ColorPreference color;
|
||||
public static boolean disableM3PillActiveIndicator;
|
||||
public static boolean showNavigationLabels;
|
||||
public static boolean displayPronounsInTimelines, displayPronounsInThreads, displayPronounsInUserListings;
|
||||
public static boolean overlayMedia;
|
||||
public static boolean showSuicideHelp;
|
||||
public static boolean underlinedLinks;
|
||||
public static ColorPreference color;
|
||||
public static boolean likeIcon;
|
||||
|
||||
// MOSHIDON
|
||||
public static boolean showDividers;
|
||||
public static boolean relocatePublishButton;
|
||||
public static boolean defaultToUnlistedReplies;
|
||||
public static boolean doubleTapToSearch;
|
||||
public static boolean doubleTapToSwipe;
|
||||
public static boolean confirmBeforeReblog;
|
||||
public static boolean hapticFeedback;
|
||||
public static boolean replyLineAboveHeader;
|
||||
public static boolean swapBookmarkWithBoostAction;
|
||||
public static boolean loadRemoteAccountFollowers;
|
||||
public static boolean mentionRebloggerAutomatically;
|
||||
public static boolean showPostsWithoutAlt;
|
||||
public static boolean showMediaPreview;
|
||||
public static boolean removeTrackingParams;
|
||||
public static boolean enhanceTextSize;
|
||||
|
||||
public static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPreReplyPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
|
||||
public static <T> T fromJson(String json, Type type, T orElse){
|
||||
if(json==null) return orElse;
|
||||
try{
|
||||
|
@ -124,6 +111,7 @@ public class GlobalUserPreferences{
|
|||
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
|
||||
toolbarMarquee=prefs.getBoolean("toolbarMarquee", true);
|
||||
disableSwipe=prefs.getBoolean("disableSwipe", false);
|
||||
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
|
||||
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false);
|
||||
translateButtonOpenedOnly=prefs.getBoolean("translateButtonOpenedOnly", false);
|
||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||
|
@ -135,38 +123,33 @@ public class GlobalUserPreferences{
|
|||
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
||||
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
||||
autoHideFab=prefs.getBoolean("autoHideFab", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
allowRemoteLoading=prefs.getBoolean("allowRemoteLoading", true);
|
||||
autoRevealEqualSpoilers=AutoRevealMode.valueOf(prefs.getString("autoRevealEqualSpoilers", AutoRevealMode.THREADS.name()));
|
||||
forwardReportDefault=prefs.getBoolean("forwardReportDefault", true);
|
||||
disableM3PillActiveIndicator=prefs.getBoolean("disableM3PillActiveIndicator", false);
|
||||
showNavigationLabels=prefs.getBoolean("showNavigationLabels", true);
|
||||
displayPronounsInTimelines=prefs.getBoolean("displayPronounsInTimelines", true);
|
||||
displayPronounsInThreads=prefs.getBoolean("displayPronounsInThreads", true);
|
||||
displayPronounsInUserListings=prefs.getBoolean("displayPronounsInUserListings", true);
|
||||
overlayMedia=prefs.getBoolean("overlayMedia", false);
|
||||
showSuicideHelp=prefs.getBoolean("showSuicideHelp", true);
|
||||
underlinedLinks=prefs.getBoolean("underlinedLinks", true);
|
||||
color=ColorPreference.valueOf(prefs.getString("color", MATERIAL3.name()));
|
||||
likeIcon=prefs.getBoolean("likeIcon", false);
|
||||
|
||||
// MOSHIDON
|
||||
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false);
|
||||
showDividers =prefs.getBoolean("showDividers", false);
|
||||
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
|
||||
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
|
||||
defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false);
|
||||
doubleTapToSearch =prefs.getBoolean("doubleTapToSearch", true);
|
||||
doubleTapToSwipe =prefs.getBoolean("doubleTapToSwipe", true);
|
||||
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
|
||||
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
|
||||
hapticFeedback=prefs.getBoolean("hapticFeedback", true);
|
||||
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
|
||||
loadRemoteAccountFollowers=prefs.getBoolean("loadRemoteAccountFollowers", true);
|
||||
mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false);
|
||||
showPostsWithoutAlt=prefs.getBoolean("showPostsWithoutAlt", true);
|
||||
showMediaPreview=prefs.getBoolean("showMediaPreview", true);
|
||||
removeTrackingParams=prefs.getBoolean("removeTrackingParams", true);
|
||||
enhanceTextSize=prefs.getBoolean("enhanceTextSize", false);
|
||||
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
|
||||
|
||||
if (prefs.contains("prefixRepliesWithRe")) {
|
||||
prefixReplies = prefs.getBoolean("prefixRepliesWithRe", false)
|
||||
? PrefixRepliesMode.TO_OTHERS : PrefixRepliesMode.NEVER;
|
||||
|
@ -176,11 +159,18 @@ public class GlobalUserPreferences{
|
|||
.apply();
|
||||
}
|
||||
|
||||
int migrationLevel=prefs.getInt("migrationLevel", BuildConfig.VERSION_CODE);
|
||||
if(migrationLevel < 61)
|
||||
migrateToUpstreamVersion61();
|
||||
if(migrationLevel < BuildConfig.VERSION_CODE)
|
||||
prefs.edit().putInt("migrationLevel", BuildConfig.VERSION_CODE).apply();
|
||||
try {
|
||||
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.MATERIAL3.name()));
|
||||
}else{
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PURPLE.name()));
|
||||
}
|
||||
} catch (IllegalArgumentException|ClassCastException ignored) {
|
||||
// invalid color name or color was previously saved as integer
|
||||
color=ColorPreference.PURPLE;
|
||||
}
|
||||
|
||||
if(prefs.getInt("migrationLevel", 0) < 61) migrateToUpstreamVersion61();
|
||||
}
|
||||
|
||||
public static void save(){
|
||||
|
@ -210,89 +200,35 @@ public class GlobalUserPreferences{
|
|||
.putBoolean("collapseLongPosts", collapseLongPosts)
|
||||
.putBoolean("spectatorMode", spectatorMode)
|
||||
.putBoolean("autoHideFab", autoHideFab)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putString("color", color.name())
|
||||
.putBoolean("allowRemoteLoading", allowRemoteLoading)
|
||||
.putString("autoRevealEqualSpoilers", autoRevealEqualSpoilers.name())
|
||||
.putBoolean("forwardReportDefault", forwardReportDefault)
|
||||
.putBoolean("disableM3PillActiveIndicator", disableM3PillActiveIndicator)
|
||||
.putBoolean("showNavigationLabels", showNavigationLabels)
|
||||
.putBoolean("displayPronounsInTimelines", displayPronounsInTimelines)
|
||||
.putBoolean("displayPronounsInThreads", displayPronounsInThreads)
|
||||
.putBoolean("displayPronounsInUserListings", displayPronounsInUserListings)
|
||||
.putBoolean("overlayMedia", overlayMedia)
|
||||
.putBoolean("showSuicideHelp", showSuicideHelp)
|
||||
.putBoolean("underlinedLinks", underlinedLinks)
|
||||
.putString("color", color.name())
|
||||
.putBoolean("likeIcon", likeIcon)
|
||||
|
||||
// MOSHIDON
|
||||
.putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies)
|
||||
.putBoolean("doubleTapToSearch", doubleTapToSearch)
|
||||
.putBoolean("doubleTapToSwipe", doubleTapToSwipe)
|
||||
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
|
||||
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
|
||||
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
|
||||
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
|
||||
.putBoolean("hapticFeedback", hapticFeedback)
|
||||
.putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers)
|
||||
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
|
||||
.putBoolean("showDividers", showDividers)
|
||||
.putBoolean("relocatePublishButton", relocatePublishButton)
|
||||
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
|
||||
.putBoolean("showPostsWithoutAlt", showPostsWithoutAlt)
|
||||
.putBoolean("showMediaPreview", showMediaPreview)
|
||||
.putBoolean("removeTrackingParams", removeTrackingParams)
|
||||
.putBoolean("enhanceTextSize", enhanceTextSize)
|
||||
.putInt("theme", theme.ordinal())
|
||||
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static boolean isOptedOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||
if(getPreReplyPrefs().getBoolean("opt_out_"+type, false))
|
||||
return true;
|
||||
if(account==null)
|
||||
return false;
|
||||
String accountKey=account.acct;
|
||||
if(!accountKey.contains("@"))
|
||||
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||
return getPreReplyPrefs().getBoolean("opt_out_"+type+"_"+accountKey.toLowerCase(), false);
|
||||
}
|
||||
|
||||
public static void optOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
|
||||
String key;
|
||||
if(account==null){
|
||||
key="opt_out_"+type;
|
||||
}else{
|
||||
String accountKey=account.acct;
|
||||
if(!accountKey.contains("@"))
|
||||
accountKey+="@"+AccountSessionManager.get(accountID).domain;
|
||||
key="opt_out_"+type+"_"+accountKey.toLowerCase();
|
||||
}
|
||||
getPreReplyPrefs().edit().putBoolean(key, true).apply();
|
||||
}
|
||||
|
||||
public enum ThemePreference{
|
||||
AUTO,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
public enum PreReplySheetType{
|
||||
OLD_POST,
|
||||
NON_MUTUAL
|
||||
}
|
||||
|
||||
public enum AutoRevealMode {
|
||||
NEVER,
|
||||
THREADS,
|
||||
DISCUSSIONS
|
||||
}
|
||||
|
||||
public enum PrefixRepliesMode {
|
||||
NEVER,
|
||||
ALWAYS,
|
||||
TO_OTHERS
|
||||
}
|
||||
|
||||
|
||||
//region preferences migrations
|
||||
|
||||
private static void migrateToUpstreamVersion61(){
|
||||
Log.d(TAG, "Migrating preferences to upstream version 61!!");
|
||||
|
||||
|
@ -339,7 +275,53 @@ public class GlobalUserPreferences{
|
|||
|
||||
localPrefs.save();
|
||||
}
|
||||
|
||||
prefs.edit().putInt("migrationLevel", 61).apply();
|
||||
}
|
||||
|
||||
//endregion
|
||||
public enum ColorPreference{
|
||||
MATERIAL3,
|
||||
PINK,
|
||||
PURPLE,
|
||||
GREEN,
|
||||
BLUE,
|
||||
BROWN,
|
||||
RED,
|
||||
YELLOW,
|
||||
NORD,
|
||||
WHITE;
|
||||
|
||||
public @StringRes int getName() {
|
||||
return switch(this){
|
||||
case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
case PINK -> R.string.sk_color_palette_pink;
|
||||
case PURPLE -> R.string.sk_color_palette_purple;
|
||||
case GREEN -> R.string.sk_color_palette_green;
|
||||
case BLUE -> R.string.sk_color_palette_blue;
|
||||
case BROWN -> R.string.sk_color_palette_brown;
|
||||
case RED -> R.string.sk_color_palette_red;
|
||||
case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
case NORD -> R.string.mo_color_palette_nord;
|
||||
case WHITE -> R.string.mo_color_palette_black_and_white;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum ThemePreference{
|
||||
AUTO,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
public enum AutoRevealMode {
|
||||
NEVER,
|
||||
THREADS,
|
||||
DISCUSSIONS
|
||||
}
|
||||
|
||||
public enum PrefixRepliesMode {
|
||||
NEVER,
|
||||
ALWAYS,
|
||||
TO_OTHERS
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,20 +7,15 @@ import android.Manifest;
|
|||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.net.Uri;
|
||||
import android.os.BadParcelableException;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
|
@ -44,46 +39,62 @@ import org.joinmastodon.android.utils.ProvidesAssistContent;
|
|||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Instant;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||
private static final String TAG="MainActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
AccountSession session=getCurrentSession();
|
||||
UiUtils.setUserPreferredTheme(this, session);
|
||||
UiUtils.setUserPreferredTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Thread.UncaughtExceptionHandler defaultHandler=Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e)->{
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "crash.log");
|
||||
try(FileOutputStream out=new FileOutputStream(file)){
|
||||
PrintWriter writer=new PrintWriter(out);
|
||||
writer.println(BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")");
|
||||
writer.println(Instant.now().toString());
|
||||
writer.println();
|
||||
e.printStackTrace(writer);
|
||||
writer.flush();
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error writing crash.log", x);
|
||||
}finally{
|
||||
defaultHandler.uncaughtException(t, e);
|
||||
}
|
||||
});
|
||||
|
||||
if(savedInstanceState==null){
|
||||
restartHomeFragment();
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
||||
}else{
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
AccountSessionManager.getInstance()
|
||||
.setLastActiveAccountID(intent.getStringExtra("account"));
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
|
||||
AccountSessionManager.getInstance().getLastActiveAccount());
|
||||
showFragmentForExternalShare(intent.getExtras());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!hasNotification) args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
if(fromNotification && hasNotification){
|
||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
} else if (intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
} else {
|
||||
showFragmentClearingBackStack(fragment);
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
|
@ -115,6 +126,8 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||
fragment.setArguments(args);
|
||||
showFragmentClearingBackStack(fragment);
|
||||
}
|
||||
}else if(intent.getBooleanExtra("compose", false)){
|
||||
showCompose();
|
||||
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
|
||||
|
@ -134,11 +147,11 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||
session=AccountSessionManager.get(accountID);
|
||||
if(session==null || !session.activated)
|
||||
return;
|
||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
|
||||
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
|
||||
}
|
||||
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
|
||||
new GetSearchResults(q, type, true, null, 0, 0)
|
||||
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
|
||||
new GetSearchResults(q, null, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
|
@ -189,6 +202,17 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||
showFragment(fragment);
|
||||
}
|
||||
|
||||
private void showCompose(){
|
||||
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if(session==null || !session.activated)
|
||||
return;
|
||||
ComposeFragment compose=new ComposeFragment();
|
||||
Bundle composeArgs=new Bundle();
|
||||
composeArgs.putString("account", session.getID());
|
||||
compose.setArguments(composeArgs);
|
||||
showFragment(compose);
|
||||
}
|
||||
|
||||
private void maybeRequestNotificationsPermission(){
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)!=PackageManager.PERMISSION_GRANTED){
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
|
||||
|
@ -261,101 +285,4 @@ public class MainActivity extends FragmentStackActivity implements ProvidesAssis
|
|||
Fragment fragment = getCurrentFragment();
|
||||
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
|
||||
}
|
||||
|
||||
public AccountSession getCurrentSession(){
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
return AccountSessionManager.getInstance()
|
||||
.getAccount(intent.getStringExtra("account"));
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!hasNotification) args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
public void restartActivity(){
|
||||
finish();
|
||||
startActivity(new Intent(this, MainActivity.class));
|
||||
}
|
||||
|
||||
public void restartHomeFragment(){
|
||||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new CustomWelcomeFragment());
|
||||
}else{
|
||||
AccountSession session;
|
||||
Bundle args=new Bundle();
|
||||
Intent intent=getIntent();
|
||||
if(intent.hasExtra("fromExternalShare")) {
|
||||
AccountSessionManager.getInstance()
|
||||
.setLastActiveAccountID(intent.getStringExtra("account"));
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
|
||||
AccountSessionManager.getInstance().getLastActiveAccount());
|
||||
showFragmentForExternalShare(intent.getExtras());
|
||||
return;
|
||||
}
|
||||
|
||||
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
|
||||
boolean hasNotification = intent.hasExtra("notification");
|
||||
if(fromNotification){
|
||||
String accountID=intent.getStringExtra("accountID");
|
||||
try{
|
||||
session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(!hasNotification) args.putString("tab", "notifications");
|
||||
}catch(IllegalStateException x){
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
}else{
|
||||
session=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
}
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
|
||||
args.putString("account", session.getID());
|
||||
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
|
||||
fragment.setArguments(args);
|
||||
if(fromNotification && hasNotification){
|
||||
// Parcelables might not be compatible across app versions so this protects against possible crashes
|
||||
// when a notification was received, then the app was updated, and then the user opened the notification
|
||||
try{
|
||||
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||
showFragmentForNotification(notification, session.getID());
|
||||
}catch(BadParcelableException x){
|
||||
Log.w(TAG, x);
|
||||
}
|
||||
} else if (Intent.ACTION_VIEW.equals(intent.getAction())){
|
||||
handleURL(intent.getData(), null);
|
||||
} else {
|
||||
showFragmentClearingBackStack(fragment);
|
||||
maybeRequestNotificationsPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
if (!GlobalUserPreferences.enhanceTextSize) {
|
||||
super.attachBaseContext(base);
|
||||
return;
|
||||
}
|
||||
|
||||
final Configuration override = new Configuration(base.getResources().getConfiguration());
|
||||
|
||||
// This is the font multiplier, which should be multiplied by, because the system settings also play a role here
|
||||
override.fontScale *= 1.15f;
|
||||
final Context newBase = base.createConfigurationContext(override);
|
||||
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.content.Context;
|
|||
import android.webkit.WebView;
|
||||
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.utils.UnifiedPushHelper;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.NetworkUtils;
|
||||
|
@ -26,13 +25,9 @@ public class MastodonApp extends Application{
|
|||
params.diskCacheSize=100*1024*1024;
|
||||
params.maxMemoryCacheSize=Integer.MAX_VALUE;
|
||||
ImageCache.setParams(params);
|
||||
NetworkUtils.setUserAgent("MoshidonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
NetworkUtils.setUserAgent("MastodonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
|
||||
if (UnifiedPushHelper.isUnifiedPushEnabled(this)){
|
||||
UnifiedPushHelper.registerAllAccounts(this);
|
||||
} else {
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
}
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
GlobalUserPreferences.load();
|
||||
if(BuildConfig.DEBUG){
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
|
|
|
@ -17,6 +17,7 @@ import android.graphics.drawable.Drawable;
|
|||
import android.opengl.Visibility;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -37,6 +38,7 @@ import org.joinmastodon.android.model.PushNotification;
|
|||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
|
@ -101,7 +103,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||
}
|
||||
String accountID=account.getID();
|
||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||
new GetNotificationByID(pn.notificationId)
|
||||
new GetNotificationByID(pn.notificationId+"")
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(org.joinmastodon.android.model.Notification result){
|
||||
|
@ -133,11 +135,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||
|
||||
if(intent.hasExtra("notification")){
|
||||
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
|
||||
|
||||
String statusID = null;
|
||||
if(notification != null && notification.status != null)
|
||||
statusID=notification.status.id;
|
||||
|
||||
String statusID=notification.status.id;
|
||||
if (statusID != null) {
|
||||
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
|
||||
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
|
||||
|
@ -158,12 +156,12 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||
}
|
||||
}
|
||||
|
||||
public void notifyUnifiedPush(Context context, AccountSession account, org.joinmastodon.android.model.Notification notification) {
|
||||
public void notifyUnifiedPush(Context context, String accountID, org.joinmastodon.android.model.Notification notification) {
|
||||
// push notifications are only created from the official push notification, so we create a fake from by transforming the notification
|
||||
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, account, notification), account.getID(), notification);
|
||||
PushNotificationReceiver.this.notify(context, PushNotification.fromNotification(context, notification), accountID, notification);
|
||||
}
|
||||
|
||||
void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
||||
private void notify(Context context, PushNotification pn, String accountID, org.joinmastodon.android.model.Notification notification){
|
||||
NotificationManager nm=context.getSystemService(NotificationManager.class);
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
Account self=session.self;
|
||||
|
@ -184,8 +182,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
|
||||
.map(type->{
|
||||
NotificationChannel channel=new NotificationChannel(accountID+"_"+type, context.getString(type.localizedName), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.setLightColor(context.getColor(R.color.primary_700));
|
||||
channel.enableLights(true);
|
||||
channel.setGroup(accountID);
|
||||
return channel;
|
||||
})
|
||||
|
@ -215,12 +211,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||
.setShowWhen(true)
|
||||
.setCategory(Notification.CATEGORY_SOCIAL)
|
||||
.setAutoCancel(true)
|
||||
.setLights(context.getColor(R.color.primary_700), 500, 1000)
|
||||
.setColor(context.getColor(R.color.shortcut_icon_background));
|
||||
|
||||
if (!GlobalUserPreferences.uniformNotificationIcon) {
|
||||
builder.setSmallIcon(switch (pn.notificationType) {
|
||||
case FAVORITE -> GlobalUserPreferences.likeIcon ? R.drawable.ic_fluent_heart_24_filled : R.drawable.ic_fluent_star_24_filled;
|
||||
case FAVORITE -> R.drawable.ic_fluent_star_24_filled;
|
||||
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
|
||||
case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled;
|
||||
case MENTION -> R.drawable.ic_fluent_mention_24_filled;
|
||||
|
@ -341,11 +336,11 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
req.status = initialText + input.toString();
|
||||
req.language = notification.status.language;
|
||||
req.visibility = (notification.status.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : notification.status.visibility);
|
||||
req.language = preferences.postingDefaultLanguage;
|
||||
req.visibility = preferences.postingDefaultVisibility;
|
||||
req.inReplyToId = notification.status.id;
|
||||
|
||||
if (notification.status.hasSpoiler() &&
|
||||
if (!notification.status.spoilerText.isEmpty() &&
|
||||
(GlobalUserPreferences.prefixReplies == ALWAYS
|
||||
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(notification.status.account.id)))
|
||||
&& !notification.status.spoilerText.startsWith("re: ")) {
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
package org.joinmastodon.android;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class TweakedFileProvider extends FileProvider{
|
||||
private static final String TAG="TweakedFileProvider";
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri){
|
||||
Log.d(TAG, "getType() called with: uri = ["+uri+"]");
|
||||
if(uri.getPathSegments().get(0).equals("image_cache")){
|
||||
Log.i(TAG, "getType: HERE!");
|
||||
return "image/jpeg"; // might as well be a png but image decoding APIs don't care, needs to be image/* though
|
||||
}
|
||||
return super.getType(uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
|
||||
Log.d(TAG, "query() called with: uri = ["+uri+"], projection = ["+Arrays.toString(projection)+"], selection = ["+selection+"], selectionArgs = ["+Arrays.toString(selectionArgs)+"], sortOrder = ["+sortOrder+"]");
|
||||
return super.query(uri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
|
||||
Log.d(TAG, "openFile() called with: uri = ["+uri+"], mode = ["+mode+"]");
|
||||
return super.openFile(uri, mode);
|
||||
}
|
||||
}
|
|
@ -5,22 +5,14 @@ import android.util.Log;
|
|||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotificationByID;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.unifiedpush.android.connector.FailedReason;
|
||||
import org.unifiedpush.android.connector.MessagingReceiver;
|
||||
import org.unifiedpush.android.connector.data.PublicKeySet;
|
||||
import org.unifiedpush.android.connector.data.PushEndpoint;
|
||||
import org.unifiedpush.android.connector.data.PushMessage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import kotlin.text.Charsets;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
|
@ -32,27 +24,20 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onNewEndpoint(@NotNull Context context, @NotNull PushEndpoint endpoint, @NotNull String instance) {
|
||||
public void onNewEndpoint(@NotNull Context context, @NotNull String endpoint, @NotNull String instance) {
|
||||
// Called when a new endpoint be used for sending push messages
|
||||
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint.getUrl() + " for "+ instance);
|
||||
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||
if (account != null) {
|
||||
PublicKeySet ks = endpoint.getPubKeySet();
|
||||
if (ks != null){
|
||||
account.getPushSubscriptionManager().registerAccountForPush(account.pushSubscription, true, endpoint.getUrl(), ks.getPubKey(), ks.getAuth());
|
||||
} else {
|
||||
// ks should never be null on new endpoint
|
||||
account.getPushSubscriptionManager().registerAccountForPush(account.pushSubscription, endpoint.getUrl());
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "onNewEndpoint: New Endpoint " + endpoint + " for "+ instance);
|
||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if (account != null)
|
||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegistrationFailed(@NotNull Context context, @NotNull FailedReason reason, @NotNull String instance) {
|
||||
public void onRegistrationFailed(@NotNull Context context, @NotNull String instance) {
|
||||
// called when the registration is not possible, eg. no network
|
||||
Log.d(TAG, "onRegistrationFailed: " + instance);
|
||||
//re-register for gcm
|
||||
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if (account != null)
|
||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
|
@ -62,52 +47,29 @@ public class UnifiedPushNotificationReceiver extends MessagingReceiver{
|
|||
// called when this application is unregistered from receiving push messages
|
||||
Log.d(TAG, "onUnregistered: " + instance);
|
||||
//re-register for gcm
|
||||
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||
AccountSession account = AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if (account != null)
|
||||
account.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(@NotNull Context context, @NotNull PushMessage message, @NotNull String instance) {
|
||||
Log.d(TAG, "New message for " + instance);
|
||||
public void onMessage(@NotNull Context context, @NotNull byte[] message, @NotNull String instance) {
|
||||
// Called when a new message is received. The message contains the full POST body of the push message
|
||||
AccountSession account = AccountSessionManager.getInstance().tryGetAccount(instance);
|
||||
AccountSession account = AccountSessionManager.getInstance().getAccount(instance);
|
||||
|
||||
if (account == null)
|
||||
return;
|
||||
|
||||
if (message.getDecrypted()) {
|
||||
// If the mastodon server supports the standard webpush, we can directly use the content
|
||||
Log.d(TAG, "Push message correctly decrypted");
|
||||
PushNotification pn = MastodonAPIController.gson.fromJson(new String(message.getContent(), Charsets.UTF_8), PushNotification.class);
|
||||
new GetNotificationByID(pn.notificationId)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(org.joinmastodon.android.model.Notification result){
|
||||
MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notify(context, pn, instance, result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
MastodonAPIController.runInBackground(()-> new PushNotificationReceiver().notify(context, pn, instance, null));
|
||||
}
|
||||
})
|
||||
.exec(instance);
|
||||
} else {
|
||||
// else, we have to sync with the server
|
||||
Log.d(TAG, "Server doesn't support standard webpush, fetching one notification");
|
||||
fetchOneNotification(context, account, (notif) -> () -> new PushNotificationReceiver().notifyUnifiedPush(context, account, notif));
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchOneNotification(@NotNull Context context, @NotNull AccountSession account, @NotNull Function<Notification, Runnable> callback) {
|
||||
//this is stupid
|
||||
// Mastodon stores the info to decrypt the message in the HTTP headers, which are not accessible in UnifiedPush,
|
||||
// thus it is not possible to decrypt them. SO we need to re-request them from the server and transform them later on
|
||||
// The official uses fcm and moves the headers to extra data, see
|
||||
// https://github.com/mastodon/webpush-fcm-relay/blob/cac95b28d5364b0204f629283141ac3fb749e0c5/webpush-fcm-relay.go#L116
|
||||
// https://github.com/tuskyapp/Tusky/pull/2303#issue-1112080540
|
||||
account.getCacheController().getNotifications(null, 1, false, false, true, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
result.items
|
||||
.stream()
|
||||
.findFirst()
|
||||
.ifPresent(value->MastodonAPIController.runInBackground(callback.apply(value)));
|
||||
.ifPresent(value->MastodonAPIController.runInBackground(()->new PushNotificationReceiver().notifyUnifiedPush(context, instance, value)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -9,35 +9,23 @@ import android.os.Handler;
|
|||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
|
@ -55,7 +43,6 @@ public class CacheController{
|
|||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
private List<FollowList> lists;
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
|
@ -82,11 +69,12 @@ public class CacheController{
|
|||
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
|
||||
status.postprocess();
|
||||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0) ? status.id : null;
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
result.add(status);
|
||||
}while(cursor.moveToNext());
|
||||
String _newMaxID=newMaxID;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
|
||||
return;
|
||||
}
|
||||
|
@ -98,7 +86,9 @@ public class CacheController{
|
|||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(result, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
ArrayList<Status> filtered=new ArrayList<>(result);
|
||||
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
|
||||
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
|
||||
putHomeTimeline(result, maxID==null);
|
||||
}
|
||||
|
||||
|
@ -126,14 +116,12 @@ public class CacheController{
|
|||
values.put("id", s.id);
|
||||
values.put("json", MastodonAPIController.gson.toJson(s));
|
||||
int flags=0;
|
||||
if(Objects.equals(s.hasGapAfter, s.id))
|
||||
if(s.hasGapAfter)
|
||||
flags|=POST_FLAG_GAP_AFTER;
|
||||
values.put("flags", flags);
|
||||
values.put("time", s.createdAt.getEpochSecond());
|
||||
db.insertWithOnConflict("home_timeline", null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
if(!clear)
|
||||
db.delete("home_timeline", "`id` NOT IN (SELECT `id` FROM `home_timeline` ORDER BY `time` DESC LIMIT ?)", new String[]{"1000"});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -285,28 +273,6 @@ public class CacheController{
|
|||
|
||||
public void deleteStatus(String id){
|
||||
runOnDbThread((db)->{
|
||||
String gapId=null;
|
||||
int gapFlags=0;
|
||||
// select to-be-removed and newer row
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"id", "flags"}, "`time`>=(SELECT `time` FROM `home_timeline` WHERE `id`=?)", new String[]{id}, null, null, "`time` ASC", "2")){
|
||||
boolean hadGapAfter=false;
|
||||
// always either one or two iterations (only one if there's no newer post)
|
||||
while(cursor.moveToNext()){
|
||||
String currentId=cursor.getString(0);
|
||||
int currentFlags=cursor.getInt(1);
|
||||
if(currentId.equals(id)){
|
||||
hadGapAfter=((currentFlags & POST_FLAG_GAP_AFTER)!=0);
|
||||
}else if(hadGapAfter){
|
||||
gapFlags=currentFlags|POST_FLAG_GAP_AFTER;
|
||||
gapId=currentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(gapId!=null){
|
||||
ContentValues values=new ContentValues();
|
||||
values.put("flags", gapFlags);
|
||||
db.update("home_timeline", values, "`id`=?", new String[]{gapId});
|
||||
}
|
||||
db.delete("home_timeline", "`id`=?", new String[]{id});
|
||||
});
|
||||
}
|
||||
|
@ -360,99 +326,6 @@ public class CacheController{
|
|||
}, 0);
|
||||
}
|
||||
|
||||
public void reloadLists(Callback<List<FollowList>> callback){
|
||||
new GetLists()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
result.sort(Comparator.comparing(l->l.title));
|
||||
lists=result;
|
||||
if(callback!=null)
|
||||
callback.onSuccess(result);
|
||||
writeListsToFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(callback!=null)
|
||||
callback.onError(error);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<FollowList> loadListsFromFile(){
|
||||
File file=getListsFile();
|
||||
if(!file.exists())
|
||||
return null;
|
||||
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
|
||||
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "failed to read lists from cache file", x);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeListsToFile(){
|
||||
databaseThread.postRunnable(()->{
|
||||
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
|
||||
MastodonAPIController.gson.toJson(lists, out);
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "failed to write lists to cache file", x);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public void getLists(Callback<List<FollowList>> callback){
|
||||
if(lists!=null){
|
||||
if(callback!=null)
|
||||
callback.onSuccess(lists);
|
||||
return;
|
||||
}
|
||||
databaseThread.postRunnable(()->{
|
||||
List<FollowList> lists=loadListsFromFile();
|
||||
if(lists!=null){
|
||||
this.lists=lists;
|
||||
if(callback!=null)
|
||||
uiHandler.post(()->callback.onSuccess(lists));
|
||||
return;
|
||||
}
|
||||
reloadLists(callback);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public File getListsFile(){
|
||||
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
|
||||
}
|
||||
|
||||
public void addList(FollowList list){
|
||||
if(lists==null)
|
||||
return;
|
||||
lists.add(list);
|
||||
lists.sort(Comparator.comparing(l->l.title));
|
||||
writeListsToFile();
|
||||
}
|
||||
|
||||
public void deleteList(String id){
|
||||
if(lists==null)
|
||||
return;
|
||||
lists.removeIf(l->l.id.equals(id));
|
||||
writeListsToFile();
|
||||
}
|
||||
|
||||
public void updateList(FollowList list){
|
||||
if(lists==null)
|
||||
return;
|
||||
for(int i=0;i<lists.size();i++){
|
||||
if(lists.get(i).id.equals(list.id)){
|
||||
lists.set(i, list);
|
||||
lists.sort(Comparator.comparing(l->l.title));
|
||||
writeListsToFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DatabaseHelper extends SQLiteOpenHelper{
|
||||
|
||||
public DatabaseHelper(){
|
||||
|
|
|
@ -54,9 +54,7 @@ public class MastodonAPIController{
|
|||
.create();
|
||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||
.connectTimeout(60, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
private AccountSession session;
|
||||
|
@ -91,11 +89,7 @@ public class MastodonAPIController{
|
|||
final boolean isBad = host == null || badDomains.stream().anyMatch(h -> h.equalsIgnoreCase(host) || host.toLowerCase().endsWith("." + h));
|
||||
thread.postRunnable(()->{
|
||||
try{
|
||||
if(isBad){
|
||||
Log.i(TAG, "submitRequest: refusing to connect to bad domain: " + host);
|
||||
throw new IllegalArgumentException("Failed to connect to domain");
|
||||
}
|
||||
|
||||
// if (isBad) throw new IllegalArgumentException();
|
||||
if(req.canceled)
|
||||
return;
|
||||
Request.Builder builder=new Request.Builder()
|
||||
|
@ -119,24 +113,24 @@ public class MastodonAPIController{
|
|||
}
|
||||
|
||||
Request hreq=builder.build();
|
||||
OkHttpClient client=req.timeout>0
|
||||
? httpClient.newBuilder().readTimeout(req.timeout, TimeUnit.MILLISECONDS).build()
|
||||
: httpClient;
|
||||
Call call=client.newCall(hreq);
|
||||
Call call=httpClient.newCall(hreq);
|
||||
synchronized(req){
|
||||
req.okhttpCall=call;
|
||||
}
|
||||
if(req.timeout>0){
|
||||
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, logTag(session)+"Sending request: "+hreq);
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
|
||||
|
||||
call.enqueue(new Callback(){
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e){
|
||||
if(req.canceled)
|
||||
if(call.isCanceled())
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, logTag(session)+""+hreq+" failed", e);
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" failed", e);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
|
@ -145,10 +139,10 @@ public class MastodonAPIController{
|
|||
|
||||
@Override
|
||||
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
|
||||
if(req.canceled)
|
||||
if(call.isCanceled())
|
||||
return;
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+hreq+" received response: "+response);
|
||||
synchronized(req){
|
||||
req.okhttpCall=null;
|
||||
}
|
||||
|
@ -159,7 +153,7 @@ public class MastodonAPIController{
|
|||
try{
|
||||
if(BuildConfig.DEBUG){
|
||||
JsonElement respJson=JsonParser.parseReader(reader);
|
||||
Log.d(TAG, logTag(session)+"response body: "+respJson);
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else if(req.respClass!=null)
|
||||
|
@ -181,7 +175,7 @@ public class MastodonAPIController{
|
|||
return;
|
||||
}
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, logTag(session)+response+" error parsing or reading body", x);
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
@ -190,19 +184,19 @@ public class MastodonAPIController{
|
|||
req.validateAndPostprocessResponse(respObj, response);
|
||||
}catch(IOException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, logTag(session)+response+" error post-processing or validating response", x);
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
|
||||
req.onError(x.getLocalizedMessage(), response.code(), x);
|
||||
return;
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.d(TAG, logTag(session)+response+" parsed successfully: "+respObj);
|
||||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" parsed successfully: "+respObj);
|
||||
|
||||
req.onSuccess(respObj);
|
||||
}else{
|
||||
try{
|
||||
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
|
||||
Log.w(TAG, logTag(session)+response+" received error: "+error);
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
|
||||
if(error.has("details")){
|
||||
MastodonDetailedErrorResponse err=new MastodonDetailedErrorResponse(error.get("error").getAsString(), response.code(), null);
|
||||
HashMap<String, List<MastodonDetailedErrorResponse.FieldError>> details=new HashMap<>();
|
||||
|
@ -226,11 +220,7 @@ public class MastodonAPIController{
|
|||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
req.onError(response.code()+" "+response.message(), response.code(), x);
|
||||
}catch(Exception x){
|
||||
if (response.code() == 501){
|
||||
req.onError("API route not implemented: " + response.request().url(), response.code(), x);
|
||||
} else {
|
||||
req.onError("Error parsing an API error", response.code(), x);
|
||||
}
|
||||
req.onError("Error parsing an API error", response.code(), x);
|
||||
}
|
||||
}
|
||||
}catch(Exception x){
|
||||
|
@ -241,7 +231,7 @@ public class MastodonAPIController{
|
|||
});
|
||||
}catch(Exception x){
|
||||
if(BuildConfig.DEBUG)
|
||||
Log.w(TAG, logTag(session)+"error creating and sending http request", x);
|
||||
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
|
||||
req.onError(x.getLocalizedMessage(), 0, x);
|
||||
}
|
||||
}, 0);
|
||||
|
@ -254,8 +244,4 @@ public class MastodonAPIController{
|
|||
public static OkHttpClient getHttpClient(){
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private static String logTag(AccountSession session){
|
||||
return "["+(session==null ? "no-auth" : session.getID())+"] ";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,8 +37,6 @@ import okhttp3.Response;
|
|||
public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
private static final String TAG="MastodonAPIRequest";
|
||||
|
||||
private static MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
|
||||
|
||||
private String domain;
|
||||
private AccountSession account;
|
||||
private String path;
|
||||
|
@ -97,14 +95,14 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||
|
||||
public MastodonAPIRequest<T> execNoAuth(String domain){
|
||||
this.domain=domain;
|
||||
unauthenticatedApiController.submitRequest(this);
|
||||
AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> exec(String domain, Token token){
|
||||
this.domain=domain;
|
||||
this.token=token;
|
||||
unauthenticatedApiController.submitRequest(this);
|
||||
AccountSessionManager.getInstance().getUnauthenticatedApiController().submitRequest(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -139,7 +137,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||
return this;
|
||||
}
|
||||
|
||||
public void setRequestBody(Object body){
|
||||
protected void setRequestBody(Object body){
|
||||
requestBody=body;
|
||||
}
|
||||
|
||||
|
@ -155,9 +153,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||
headers.put(key, value);
|
||||
}
|
||||
|
||||
public MastodonAPIRequest<T> setTimeout(long timeout){
|
||||
protected void setTimeout(long timeout){
|
||||
this.timeout=timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected String getPathPrefix(){
|
||||
|
@ -182,8 +179,6 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||
}
|
||||
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
if(requestBody instanceof RequestBody rb)
|
||||
return rb;
|
||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||
}
|
||||
|
||||
|
|
|
@ -125,16 +125,17 @@ public class PushSubscriptionManager{
|
|||
// this function is used for registering push notifications using FCM
|
||||
// to avoid NonFreeNet in F-Droid, this registration is disabled in it
|
||||
// see https://github.com/LucasGGamerM/moshidon/issues/206 for more context
|
||||
if(BuildConfig.BUILD_TYPE.equals("fdroidRelease") || TextUtils.isEmpty(deviceToken)){
|
||||
Log.d(TAG, "Skipping registering for FCM push notifications");
|
||||
if(BuildConfig.BUILD_TYPE.equals("fdroidRelease"))
|
||||
return;
|
||||
}
|
||||
|
||||
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/";
|
||||
if(TextUtils.isEmpty(deviceToken))
|
||||
throw new IllegalStateException("No device push token available");
|
||||
String endpoint = "https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
|
||||
registerAccountForPush(subscription, endpoint);
|
||||
}
|
||||
|
||||
public void registerAccountForPush(PushSubscription subscription, String endpoint){
|
||||
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Log.d(TAG, "registerAccountForPush: started for "+accountID);
|
||||
String encodedPublicKey, encodedAuthKey, pushAccountID;
|
||||
|
@ -163,26 +164,9 @@ public class PushSubscriptionManager{
|
|||
Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
|
||||
return;
|
||||
}
|
||||
|
||||
//work-around for adding the randomAccountId
|
||||
String newEndpoint = endpoint;
|
||||
Boolean standard = true;
|
||||
if (endpoint.startsWith("https://app.joinmastodon.org/relay-to/fcm/")){
|
||||
newEndpoint+=pushAccountID;
|
||||
standard = false;
|
||||
}
|
||||
|
||||
registerAccountForPush(subscription, standard, newEndpoint, encodedPublicKey, encodedAuthKey);
|
||||
});
|
||||
}
|
||||
|
||||
public void registerAccountForPush(PushSubscription subscription, Boolean standard, String endpoint, String p256dh, String auth){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Log.d(TAG, "registerAccountForPush: started for "+accountID);
|
||||
new RegisterForPushNotifications(endpoint,
|
||||
standard,
|
||||
p256dh,
|
||||
auth,
|
||||
encodedPublicKey,
|
||||
encodedAuthKey,
|
||||
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
|
||||
subscription==null ? PushSubscription.Policy.ALL : subscription.policy)
|
||||
.setCallback(new Callback<>(){
|
||||
|
|
|
@ -6,25 +6,12 @@ import org.joinmastodon.android.E;
|
|||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusMuted;
|
||||
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
||||
import org.joinmastodon.android.events.ReblogDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.EmojiReaction;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
|
@ -36,7 +23,6 @@ public class StatusInteractionController{
|
|||
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
|
||||
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
|
||||
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
|
||||
private final HashMap<String, SetStatusMuted> runningMuteRequests=new HashMap<>();
|
||||
|
||||
public StatusInteractionController(String accountID, boolean updateCounters) {
|
||||
this.accountID=accountID;
|
||||
|
@ -51,9 +37,6 @@ public class StatusInteractionController{
|
|||
if(!Looper.getMainLooper().isCurrentThread())
|
||||
throw new IllegalStateException("Can only be called from main thread");
|
||||
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
Instance instance=session.getInstance().get();
|
||||
|
||||
SetStatusFavorited current=runningFavoriteRequests.remove(status.id);
|
||||
if(current!=null){
|
||||
current.cancel();
|
||||
|
@ -65,8 +48,7 @@ public class StatusInteractionController{
|
|||
runningFavoriteRequests.remove(status.id);
|
||||
result.favouritesCount = Math.max(0, status.favouritesCount + (favorited ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
if(instance.isIceshrimpJs()) E.post(new EmojiReactionsUpdatedEvent(status.id, result.reactions, false, null));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -75,59 +57,13 @@ public class StatusInteractionController{
|
|||
error.showToast(MastodonApp.context);
|
||||
status.favourited=!favorited;
|
||||
cb.accept(status);
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if(instance.isIceshrimpJs()) E.post(new EmojiReactionsUpdatedEvent(status.id, status.reactions, false, null));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningFavoriteRequests.put(status.id, req);
|
||||
status.favourited=favorited;
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
|
||||
if(instance.configuration==null || instance.configuration.reactions==null)
|
||||
return;
|
||||
|
||||
String defaultReactionEmojiRaw=instance.configuration.reactions.defaultReaction;
|
||||
if(!instance.isIceshrimpJs() || defaultReactionEmojiRaw==null)
|
||||
return;
|
||||
|
||||
boolean reactionIsCustom=defaultReactionEmojiRaw.startsWith(":");
|
||||
String defaultReactionEmoji=reactionIsCustom ? defaultReactionEmojiRaw.substring(1, defaultReactionEmojiRaw.length()-1) : defaultReactionEmojiRaw;
|
||||
ArrayList<EmojiReaction> reactions=new ArrayList<>(status.reactions.size());
|
||||
for(EmojiReaction reaction:status.reactions){
|
||||
reactions.add(reaction.copy());
|
||||
}
|
||||
Optional<EmojiReaction> existingReaction=reactions.stream().filter(r->r.me).findFirst();
|
||||
Optional<EmojiReaction> existingDefaultReaction=reactions.stream().filter(r->r.name.equals(defaultReactionEmoji)).findFirst();
|
||||
if(existingReaction.isPresent() && !favorited){
|
||||
existingReaction.get().me=false;
|
||||
existingReaction.get().count--;
|
||||
existingReaction.get().pendingChange=true;
|
||||
}else if(existingDefaultReaction.isPresent() && favorited){
|
||||
existingDefaultReaction.get().count++;
|
||||
existingDefaultReaction.get().me=true;
|
||||
existingDefaultReaction.get().pendingChange=true;
|
||||
}else if(favorited){
|
||||
EmojiReaction reaction=null;
|
||||
if(reactionIsCustom){
|
||||
List<EmojiCategory> customEmojis=AccountSessionManager.getInstance().getCustomEmojis(session.domain);
|
||||
for(EmojiCategory category:customEmojis){
|
||||
for(Emoji emoji:category.emojis){
|
||||
if(emoji.shortcode.equals(defaultReactionEmoji)){
|
||||
reaction=EmojiReaction.of(emoji, session.self);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(reaction==null)
|
||||
reaction=EmojiReaction.of(defaultReactionEmoji, session.self);
|
||||
}else{
|
||||
reaction=EmojiReaction.of(defaultReactionEmoji, session.self);
|
||||
}
|
||||
reaction.pendingChange=true;
|
||||
reactions.add(reaction);
|
||||
}
|
||||
E.post(new EmojiReactionsUpdatedEvent(status.id, reactions, false, null));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
|
||||
|
@ -142,15 +78,11 @@ public class StatusInteractionController{
|
|||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status reblog){
|
||||
Status result=reblog.getContentStatus();
|
||||
Status result = reblog.getContentStatus();
|
||||
runningReblogRequests.remove(status.id);
|
||||
result.reblogsCount = Math.max(0, status.reblogsCount + (reblogged ? 1 : -1));
|
||||
cb.accept(result);
|
||||
if(updateCounters){
|
||||
E.post(new StatusCountersUpdatedEvent(result));
|
||||
if(reblogged) E.post(new StatusCreatedEvent(reblog, accountID));
|
||||
else E.post(new ReblogDeletedEvent(status.id, accountID));
|
||||
}
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -159,13 +91,13 @@ public class StatusInteractionController{
|
|||
error.showToast(MastodonApp.context);
|
||||
status.reblogged=!reblogged;
|
||||
cb.accept(status);
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningReblogRequests.put(status.id, req);
|
||||
status.reblogged=reblogged;
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
|
||||
public void setBookmarked(Status status, boolean bookmarked){
|
||||
|
@ -186,7 +118,7 @@ public class StatusInteractionController{
|
|||
public void onSuccess(Status result){
|
||||
runningBookmarkRequests.remove(status.id);
|
||||
cb.accept(result);
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -195,12 +127,12 @@ public class StatusInteractionController{
|
|||
error.showToast(MastodonApp.context);
|
||||
status.bookmarked=!bookmarked;
|
||||
cb.accept(status);
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
runningBookmarkRequests.put(status.id, req);
|
||||
status.bookmarked=bookmarked;
|
||||
if(updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +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.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BiteAccount extends MastodonAPIRequest{
|
||||
public BiteAccount(String id){
|
||||
super(HttpMethod.POST, "/users/"+id+"/bite", BiteAccount.class);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.model.BaseModel;
|
||||
|
||||
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
|
||||
public CheckInviteLink(String path){
|
||||
super(HttpMethod.GET, path, Response.class);
|
||||
addHeader("Accept", "application/json");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "";
|
||||
}
|
||||
|
||||
public static class Response extends BaseModel{
|
||||
@RequiredField
|
||||
public String inviteCode;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetAccountBlocks extends HeaderPaginationRequest<Account>{
|
||||
public GetAccountBlocks(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/blocks", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
|
@ -1,14 +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.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetAccountLists extends MastodonAPIRequest<List<FollowList>>{
|
||||
public GetAccountLists(String id){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/lists", new TypeToken<>(){});
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetAccountMutes extends HeaderPaginationRequest<Account>{
|
||||
public GetAccountMutes(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/mutes/", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
|
@ -4,23 +4,22 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
public class RegisterAccount extends MastodonAPIRequest<Token>{
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){
|
||||
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
|
||||
super(HttpMethod.POST, "/accounts", Token.class);
|
||||
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
|
||||
setRequestBody(new Body(username, email, password, locale, reason, timezone));
|
||||
}
|
||||
|
||||
private static class Body{
|
||||
public String username, email, password, locale, reason, timeZone, inviteCode;
|
||||
public String username, email, password, locale, reason, timeZone;
|
||||
public boolean agreement=true;
|
||||
|
||||
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){
|
||||
public Body(String username, String email, String password, String locale, String reason, String timeZone){
|
||||
this.username=username;
|
||||
this.email=email;
|
||||
this.password=password;
|
||||
this.locale=locale;
|
||||
this.reason=reason;
|
||||
this.timeZone=timeZone;
|
||||
this.inviteCode=inviteCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +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 java.util.List;
|
||||
|
||||
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
|
||||
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
|
||||
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
|
||||
addQueryParameter("q", q);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(offset>0)
|
||||
addQueryParameter("offset", offset+"");
|
||||
if(resolve)
|
||||
addQueryParameter("resolve", "true");
|
||||
if(following)
|
||||
addQueryParameter("following", "true");
|
||||
}
|
||||
}
|
|
@ -4,21 +4,15 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
|
||||
public SetAccountMuted(String id, boolean muted, long duration, boolean muteNotifications){
|
||||
public SetAccountMuted(String id, boolean muted, long duration){
|
||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
|
||||
if(muted)
|
||||
setRequestBody(new Request(duration, muteNotifications));
|
||||
else{
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
setRequestBody(new Request(duration));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public long duration;
|
||||
public boolean muteNotifications;
|
||||
public Request(long duration, boolean muteNotifications){
|
||||
public Request(long duration){
|
||||
this.duration=duration;
|
||||
this.muteNotifications=muteNotifications;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||
private Uri avatar, cover;
|
||||
private File avatarFile, coverFile;
|
||||
private List<AccountField> fields;
|
||||
private Boolean discoverable, indexable;
|
||||
|
||||
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
|
@ -42,12 +41,6 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||
this.fields=fields;
|
||||
}
|
||||
|
||||
public UpdateAccountCredentials setDiscoverableIndexable(boolean discoverable, boolean indexable){
|
||||
this.discoverable=discoverable;
|
||||
this.indexable=indexable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||
|
@ -65,21 +58,15 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
|||
}else if(coverFile!=null){
|
||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||
}
|
||||
if(fields!=null){
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
}else{
|
||||
int i=0;
|
||||
for(AccountField field:fields){
|
||||
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
||||
i++;
|
||||
}
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
}else{
|
||||
int i=0;
|
||||
for(AccountField field:fields){
|
||||
bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if(discoverable!=null)
|
||||
bldr.addFormDataPart("discoverable", discoverable.toString());
|
||||
if(indexable!=null)
|
||||
bldr.addFormDataPart("indexable", indexable.toString());
|
||||
|
||||
return bldr.build();
|
||||
}
|
||||
|
|
|
@ -6,19 +6,18 @@ import org.joinmastodon.android.model.Preferences;
|
|||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
||||
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable, Boolean indexable){
|
||||
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
setRequestBody(new Request(locked, discoverable, indexable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Boolean locked, discoverable, indexable;
|
||||
public Boolean locked, discoverable;
|
||||
public RequestSource source;
|
||||
|
||||
public Request(Boolean locked, Boolean discoverable, Boolean indexable, RequestSource source){
|
||||
public Request(Boolean locked, Boolean discoverable, RequestSource source){
|
||||
this.locked=locked;
|
||||
this.discoverable=discoverable;
|
||||
this.indexable=indexable;
|
||||
this.source=source;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,6 @@ import org.joinmastodon.android.model.FilterContext;
|
|||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
class FilterRequest{
|
||||
public String title;
|
||||
public EnumSet<FilterContext> context;
|
||||
|
|
|
@ -2,9 +2,6 @@ package org.joinmastodon.android.api.requests.filters;
|
|||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import java.util.List;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
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);
|
||||
}
|
||||
|
||||
import okhttp3.FormBody;
|
||||
|
||||
public class AddAccountsToList extends ResultlessMastodonAPIRequest{
|
||||
public AddAccountsToList(String listID, Collection<String> accountIDs){
|
||||
super(HttpMethod.POST, "/lists/"+listID+"/accounts");
|
||||
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for(String id:accountIDs){
|
||||
builder.add("account_ids[]", id);
|
||||
}
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
public static class Request{
|
||||
public List<String> accountIds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class CreateList extends MastodonAPIRequest<FollowList>{
|
||||
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.POST, "/lists", FollowList.class);
|
||||
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||
public class CreateList extends MastodonAPIRequest<ListTimeline> {
|
||||
public CreateList(String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.POST, "/lists", ListTimeline.class);
|
||||
Request req = new Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public static class Request {
|
||||
public String title;
|
||||
public FollowList.RepliesPolicy repliesPolicy;
|
||||
public boolean exclusive;
|
||||
|
||||
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
this.title=title;
|
||||
this.repliesPolicy=repliesPolicy;
|
||||
this.exclusive=exclusive;
|
||||
}
|
||||
public ListTimeline.RepliesPolicy repliesPolicy;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class DeleteList extends ResultlessMastodonAPIRequest{
|
||||
public DeleteList(String id){
|
||||
super(HttpMethod.DELETE, "/lists/"+id);
|
||||
public class DeleteList extends MastodonAPIRequest<Object> {
|
||||
public DeleteList(String id) {
|
||||
super(HttpMethod.DELETE, "/lists/" + id, Object.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class GetList extends MastodonAPIRequest<FollowList> {
|
||||
public class GetList extends MastodonAPIRequest<ListTimeline> {
|
||||
public GetList(String id) {
|
||||
super(HttpMethod.GET, "/lists/" + id, FollowList.class);
|
||||
super(HttpMethod.GET, "/lists/" + id, ListTimeline.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetListAccounts extends HeaderPaginationRequest<Account>{
|
||||
public GetListAccounts(String listID, String maxID, int limit){
|
||||
super(HttpMethod.GET, "/lists/"+listID+"/accounts", new TypeToken<>(){});
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
addQueryParameter("limit", String.valueOf(limit));
|
||||
}
|
||||
}
|
|
@ -3,11 +3,11 @@ package org.joinmastodon.android.api.requests.lists;
|
|||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
|
||||
public class GetLists extends MastodonAPIRequest<List<ListTimeline>>{
|
||||
public GetLists() {
|
||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||
}
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import java.util.List;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
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);
|
||||
}
|
||||
|
||||
import okhttp3.FormBody;
|
||||
|
||||
public class RemoveAccountsFromList extends ResultlessMastodonAPIRequest{
|
||||
public RemoveAccountsFromList(String listID, Collection<String> accountIDs){
|
||||
super(HttpMethod.DELETE, "/lists/"+listID+"/accounts");
|
||||
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for(String id:accountIDs){
|
||||
builder.add("account_ids[]", id);
|
||||
}
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
public static class Request{
|
||||
public List<String> accountIds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class UpdateList extends MastodonAPIRequest<FollowList>{
|
||||
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.PUT, "/lists/"+listID, FollowList.class);
|
||||
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String title;
|
||||
public FollowList.RepliesPolicy repliesPolicy;
|
||||
public boolean exclusive;
|
||||
|
||||
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
this.title=title;
|
||||
this.repliesPolicy=repliesPolicy;
|
||||
this.exclusive=exclusive;
|
||||
}
|
||||
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
|
||||
public UpdateList(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
|
||||
CreateList.Request req = new CreateList.Request();
|
||||
req.title = title;
|
||||
req.exclusive = exclusive;
|
||||
req.repliesPolicy = repliesPolicy;
|
||||
setRequestBody(req);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import okhttp3.MultipartBody;
|
|||
import okhttp3.RequestBody;
|
||||
|
||||
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
|
||||
private final String maxID;
|
||||
private String maxID;
|
||||
public PleromaMarkNotificationsRead(String maxID) {
|
||||
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
|
||||
this.maxID = maxID;
|
||||
|
|
|
@ -4,11 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||
import org.joinmastodon.android.model.PushSubscription;
|
||||
|
||||
public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{
|
||||
public RegisterForPushNotifications(String endpoint, Boolean standard, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
public RegisterForPushNotifications(String endpoint, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
super(HttpMethod.POST, "/push/subscription", PushSubscription.class);
|
||||
Request r=new Request();
|
||||
r.subscription.endpoint=endpoint;
|
||||
r.subscription.standard = standard;
|
||||
r.data.alerts=alerts;
|
||||
r.policy=policy;
|
||||
r.subscription.keys.p256dh=encryptionKey;
|
||||
|
@ -28,8 +27,6 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
|||
|
||||
private static class Subscription{
|
||||
public String endpoint;
|
||||
// Use standard push notifications if available
|
||||
public Boolean standard;
|
||||
public Keys keys=new Keys();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,19 +4,13 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
|||
import org.joinmastodon.android.model.SearchResults;
|
||||
|
||||
public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
|
||||
public GetSearchResults(String query, Type type, boolean resolve, String maxID, int offset, int count){
|
||||
public GetSearchResults(String query, Type type, boolean resolve){
|
||||
super(HttpMethod.GET, "/search", SearchResults.class);
|
||||
addQueryParameter("q", query);
|
||||
if(type!=null)
|
||||
addQueryParameter("type", type.name().toLowerCase());
|
||||
if(resolve)
|
||||
addQueryParameter("resolve", "true");
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(offset>0)
|
||||
addQueryParameter("offset", String.valueOf(offset));
|
||||
if(count>0)
|
||||
addQueryParameter("limit", String.valueOf(count));
|
||||
}
|
||||
|
||||
public GetSearchResults limit(int limit){
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||
|
||||
public class AkkomaTranslateStatus extends MastodonAPIRequest<AkkomaTranslation>{
|
||||
public AkkomaTranslateStatus(String id, String lang){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/translations/"+lang.toLowerCase(), AkkomaTranslation.class);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
public class BitePost extends MastodonAPIRequest{
|
||||
public BitePost(String id){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/bite", BitePost.class);
|
||||
}
|
||||
}
|
|
@ -11,11 +11,13 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
|
||||
public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public static long EPOCH_OF_THE_YEAR_FIVE_THOUSAND=95617584000000L;
|
||||
public static final Instant DRAFTS_AFTER_INSTANT=Instant.ofEpochMilli(EPOCH_OF_THE_YEAR_FIVE_THOUSAND - 1) /* end of 4999 */;
|
||||
public static final Instant DRAFTS_AFTER_INSTANT = Instant.ofEpochMilli(253370764799999L) /* end of 9998 */;
|
||||
private static final float draftFactor = 31536000000f /* one year */ / 253370764799999f /* end of 9998 */;
|
||||
|
||||
public static Instant getDraftInstant() {
|
||||
return DRAFTS_AFTER_INSTANT.plusMillis(System.currentTimeMillis());
|
||||
// returns an instant between 9999-01-01 00:00:00 and 9999-12-31 23:59:59
|
||||
// yes, this is a weird implementation for something that hardly matters
|
||||
return DRAFTS_AFTER_INSTANT.plusMillis(1 + (long) (System.currentTimeMillis() * draftFactor));
|
||||
}
|
||||
|
||||
public CreateStatus(CreateStatus.Request req, String uuid){
|
||||
|
@ -34,7 +36,6 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
|||
|
||||
public static class Request{
|
||||
public String status;
|
||||
public List<MediaAttribute> mediaAttributes;
|
||||
public List<String> mediaIds;
|
||||
public Poll poll;
|
||||
public String inReplyToId;
|
||||
|
@ -48,25 +49,11 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
|||
public String quoteId;
|
||||
public ContentType contentType;
|
||||
|
||||
public boolean preview;
|
||||
|
||||
public static class Poll{
|
||||
public ArrayList<String> options=new ArrayList<>();
|
||||
public int expiresIn;
|
||||
public boolean multiple;
|
||||
public boolean hideTotals;
|
||||
}
|
||||
|
||||
public static class MediaAttribute{
|
||||
public String id;
|
||||
public String description;
|
||||
public String focus;
|
||||
|
||||
public MediaAttribute(String id, String description, String focus){
|
||||
this.id=id;
|
||||
this.description=description;
|
||||
this.focus=focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,11 +26,6 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
|||
s.visibility=StatusPrivacy.PUBLIC;
|
||||
s.mentions=Collections.emptyList();
|
||||
s.tags=Collections.emptyList();
|
||||
if(s.poll!=null){
|
||||
s.poll.id="fakeID"+i;
|
||||
s.poll.emojis=Collections.emptyList();
|
||||
s.poll.ownVotes=Collections.emptyList();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
|
|
|
@ -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 SetStatusMuted extends MastodonAPIRequest<Status>{
|
||||
public SetStatusMuted(String id, boolean muted){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/"+(muted ? "mute" : "unmute"), Status.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.model.TranslatedStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class TranslateStatus extends MastodonAPIRequest<Translation>{
|
||||
public TranslateStatus(String id, String lang){
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/translate", Translation.class);
|
||||
setRequestBody(Map.of("lang", lang));
|
||||
}
|
||||
public class TranslateStatus extends MastodonAPIRequest<TranslatedStatus> {
|
||||
public TranslateStatus(String id) {
|
||||
super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class GetFollowedTags extends HeaderPaginationRequest<Hashtag>{
|
||||
public GetFollowedTags(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class GetTag extends MastodonAPIRequest<Hashtag>{
|
||||
public GetTag(String tag){
|
||||
super(HttpMethod.GET, "/tags/"+tag, 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 SetTagFollowed extends MastodonAPIRequest<Hashtag>{
|
||||
public SetTagFollowed(String tag, boolean followed){
|
||||
super(HttpMethod.POST, "/tags/"+tag+(followed ? "/follow" : "/unfollow"), Hashtag.class);
|
||||
setRequestBody(new Object());
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
|
|||
import java.util.List;
|
||||
|
||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID, String replyVisibility){
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
|
||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||
if(local)
|
||||
addQueryParameter("local", "true");
|
||||
|
@ -18,10 +18,6 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
|||
addQueryParameter("remote", "true");
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(!TextUtils.isEmpty(minID))
|
||||
addQueryParameter("min_id", minID);
|
||||
if(!TextUtils.isEmpty(sinceID))
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(replyVisibility != null)
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetTrendingLinksTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetTrendingLinksTimeline(@NonNull String url, String maxID, String minID, int limit){
|
||||
super(HttpMethod.GET, "/timelines/link/", new TypeToken<>(){});
|
||||
addQueryParameter("url", url);
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
}
|
|
@ -6,17 +6,10 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
|||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
@ -24,7 +17,6 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class AccountLocalPreferences{
|
||||
private final SharedPreferences prefs;
|
||||
|
@ -48,21 +40,16 @@ public class AccountLocalPreferences{
|
|||
public String publishButtonText;
|
||||
public String timelineReplyVisibility; // akkoma-only
|
||||
public boolean keepOnlyLatestNotification;
|
||||
public boolean emojiReactionsEnabled;
|
||||
public ShowEmojiReactions showEmojiReactions;
|
||||
public ColorPreference color;
|
||||
public ArrayList<Emoji> recentCustomEmoji;
|
||||
public boolean preReplySheet;
|
||||
|
||||
private final static Type recentLanguagesType=new TypeToken<ArrayList<String>>() {}.getType();
|
||||
private final static Type timelinesType=new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
||||
private final static Type recentCustomEmojiType=new TypeToken<ArrayList<Emoji>>() {}.getType();
|
||||
public boolean emojiReactionsEnabled;
|
||||
public boolean showEmojiReactionsInLists;
|
||||
|
||||
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
|
||||
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
|
||||
|
||||
// MOSHIDON
|
||||
// private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
|
||||
// public Map<String, Integer> recentEmojis;
|
||||
private final static Type notificationFiltersType = new TypeToken<PushSubscription.Alerts>() {}.getType();
|
||||
public PushSubscription.Alerts notificationFilters;
|
||||
private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
|
||||
public Map<String, Integer> recentEmojis;
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
|
||||
this.prefs=prefs;
|
||||
|
@ -71,30 +58,25 @@ public class AccountLocalPreferences{
|
|||
revealCWs=prefs.getBoolean("revealCWs", false);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||
// preReplySheet=prefs.getBoolean("preReplySheet", false);
|
||||
|
||||
// MEGALODON
|
||||
Optional<Instance> instance=session.getInstance();
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new ArrayList<>());
|
||||
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
||||
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", instance.map(Instance::isIceshrimp).orElse(false) ? ContentType.MISSKEY_MARKDOWN.name() : ContentType.PLAIN.name()));
|
||||
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", instance.map(i->!i.isIceshrimp()).orElse(false));
|
||||
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", ContentType.PLAIN.name()));
|
||||
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", true);
|
||||
timelines=fromJson(prefs.getString("timelines", null), timelinesType, TimelineDefinition.getDefaultTimelines(session.getID()));
|
||||
localOnlySupported=prefs.getBoolean("localOnlySupported", false);
|
||||
glitchInstance=prefs.getBoolean("glitchInstance", false);
|
||||
publishButtonText=prefs.getString("publishButtonText", null);
|
||||
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
|
||||
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
|
||||
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", instance.map(i->i.isAkkoma() || i.isIceshrimp()).orElse(false));
|
||||
showEmojiReactions=ShowEmojiReactions.valueOf(prefs.getString("showEmojiReactions", ShowEmojiReactions.HIDE_EMPTY.name()));
|
||||
color=prefs.contains("color") ? ColorPreference.valueOf(prefs.getString("color", null)) : null;
|
||||
recentCustomEmoji=fromJson(prefs.getString("recentCustomEmoji", null), recentCustomEmojiType, new ArrayList<>());
|
||||
emojiReactionsEnabled=prefs.getBoolean("emojiReactionsEnabled", session.getInstance().isPresent() && session.getInstance().get().isAkkoma());
|
||||
showEmojiReactionsInLists=prefs.getBoolean("showEmojiReactionsInLists", false);
|
||||
|
||||
// MOSHIDON
|
||||
// recentEmojis=fromJson(prefs.getString("recentEmojis", "{}"), recentEmojisType, new HashMap<>());
|
||||
notificationFilters=fromJson(prefs.getString("notificationFilters", gson.toJson(PushSubscription.Alerts.ofAll())), notificationFiltersType, PushSubscription.Alerts.ofAll());
|
||||
recentEmojis=fromJson(prefs.getString("recentEmojis", "{}"), recentEmojisType, new HashMap<>());
|
||||
}
|
||||
|
||||
public long getNotificationsPauseEndTime(){
|
||||
|
@ -105,10 +87,6 @@ public class AccountLocalPreferences{
|
|||
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
||||
}
|
||||
|
||||
public ColorPreference getCurrentColor(){
|
||||
return color!=null ? color : GlobalUserPreferences.color!=null ? GlobalUserPreferences.color : ColorPreference.MATERIAL3;
|
||||
}
|
||||
|
||||
public void save(){
|
||||
prefs.edit()
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
|
@ -117,9 +95,6 @@ public class AccountLocalPreferences{
|
|||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||
|
||||
//TODO figure this stuff out
|
||||
// .putBoolean("preReplySheet", preReplySheet)
|
||||
|
||||
// MEGALODON
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
|
@ -134,47 +109,10 @@ public class AccountLocalPreferences{
|
|||
.putString("timelineReplyVisibility", timelineReplyVisibility)
|
||||
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
|
||||
.putBoolean("emojiReactionsEnabled", emojiReactionsEnabled)
|
||||
.putString("showEmojiReactions", showEmojiReactions.name())
|
||||
.putString("color", color!=null ? color.name() : null)
|
||||
.putString("recentCustomEmoji", gson.toJson(recentCustomEmoji))
|
||||
.putBoolean("showEmojiReactionsInLists", showEmojiReactionsInLists)
|
||||
|
||||
// MOSHIDON
|
||||
// .putString("recentEmojis", gson.toJson(recentEmojis))
|
||||
.putString("notificationFilters", gson.toJson(notificationFilters))
|
||||
.putString("recentEmojis", gson.toJson(recentEmojis))
|
||||
.apply();
|
||||
}
|
||||
|
||||
public enum ColorPreference{
|
||||
MATERIAL3,
|
||||
PURPLE,
|
||||
PINK,
|
||||
GREEN,
|
||||
BLUE,
|
||||
BROWN,
|
||||
RED,
|
||||
YELLOW,
|
||||
NORD,
|
||||
WHITE;
|
||||
|
||||
public @StringRes int getName() {
|
||||
return switch(this){
|
||||
case MATERIAL3 -> R.string.sk_color_palette_material3;
|
||||
case PINK -> R.string.sk_color_palette_pink;
|
||||
case PURPLE -> R.string.sk_color_palette_purple;
|
||||
case GREEN -> R.string.sk_color_palette_green;
|
||||
case BLUE -> R.string.sk_color_palette_blue;
|
||||
case BROWN -> R.string.sk_color_palette_brown;
|
||||
case RED -> R.string.sk_color_palette_red;
|
||||
case YELLOW -> R.string.sk_color_palette_yellow;
|
||||
case NORD -> R.string.mo_color_palette_nord;
|
||||
case WHITE -> R.string.mo_color_palette_black_and_white;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum ShowEmojiReactions{
|
||||
HIDE_EMPTY,
|
||||
ONLY_OPENED,
|
||||
ALWAYS
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,11 @@ import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
|||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AltTextFilter;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
|
@ -36,14 +34,13 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
|||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
@ -74,7 +71,6 @@ public class AccountSession{
|
|||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
private transient List<FollowList> lists;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
|
@ -150,9 +146,6 @@ public class AccountSession{
|
|||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
|
||||
if (preferences==null)
|
||||
preferences=new Preferences();
|
||||
preferencesFromAccountSource(self);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
|
@ -223,7 +216,7 @@ public class AccountSession{
|
|||
|
||||
public void savePreferencesIfPending(){
|
||||
if(preferencesNeedSaving){
|
||||
new UpdateAccountCredentialsPreferences(preferences, self.locked, self.discoverable, self.source.indexable)
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
|
@ -259,95 +252,52 @@ public class AccountSession{
|
|||
filterStatusContainingObjects(objects, extractor, context, null);
|
||||
}
|
||||
|
||||
private boolean statusIsOnOwnProfile(Status s, Account profile){
|
||||
return self != null && profile != null && s.account != null
|
||||
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
|
||||
}
|
||||
|
||||
private boolean isFilteredType(Status s){
|
||||
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||
return (!localPreferences.showReplies && s.inReplyToId != null)
|
||||
|| (!localPreferences.showBoosts && s.reblog != null);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||
if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
|
||||
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
|
||||
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
|
||||
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
if(s.filtered==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(FilterResult filter:s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(wordFilters==null)
|
||||
return;
|
||||
for(T obj:objects){
|
||||
Status s=extractor.apply(obj);
|
||||
if(s!=null && s.filtered!=null){
|
||||
localPreferences.serverSideFiltersSupported=true;
|
||||
localPreferences.save();
|
||||
break;
|
||||
getLocalPreferences().serverSideFiltersSupported=true;
|
||||
getLocalPreferences().save();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<T> removeUs=new ArrayList<>();
|
||||
for(int i=0; i<objects.size(); i++){
|
||||
T o=objects.get(i);
|
||||
if(filterStatusContainingObject(o, extractor, context, profile)){
|
||||
Status s=extractor.apply(o);
|
||||
removeUs.add(o);
|
||||
if(s!=null && s.hasGapAfter!=null && i>0){
|
||||
// oops, we're about to remove an item that has a gap after...
|
||||
// gotta find the previous status that's not also about to be removed
|
||||
for(int j=i-1; j>=0; j--){
|
||||
T p=objects.get(j);
|
||||
Status prev=extractor.apply(objects.get(j));
|
||||
if(prev!=null && !removeUs.contains(p)){
|
||||
prev.hasGapAfter=s.hasGapAfter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
objects.removeAll(removeUs);
|
||||
}
|
||||
|
||||
public <T> boolean filterStatusContainingObject(T object, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
Status s=extractor.apply(object);
|
||||
if(s==null)
|
||||
return false;
|
||||
// don't hide own posts in own profile
|
||||
if(statusIsOnOwnProfile(s, profile))
|
||||
return false;
|
||||
if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC))
|
||||
return true;
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
// Moshidon: this code path in CustomLocalTimelines makes the app crash, so this check is here
|
||||
if (s.filtered == null)
|
||||
objects.removeIf(o->{
|
||||
Status s=extractor.apply(o);
|
||||
if(s==null)
|
||||
return false;
|
||||
for(FilterResult filter : s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE && filter.filter.context.contains(context))
|
||||
return true;
|
||||
}
|
||||
}else if(wordFilters!=null){
|
||||
for(LegacyFilter filter : wordFilters){
|
||||
// don't hide own posts in own profile
|
||||
if (statusIsOnOwnProfile.test(s))
|
||||
return false;
|
||||
for(LegacyFilter filter:wordFilters){
|
||||
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<FilterResult> getClientSideFilters(Status status) {
|
||||
List<FilterResult> filters = new ArrayList<>();
|
||||
|
||||
// filter post that have no alt text
|
||||
// it only applies when activated in the settings
|
||||
AltTextFilter altTextFilter=new AltTextFilter(FilterAction.WARN, EnumSet.allOf(FilterContext.class));
|
||||
if(altTextFilter.matches(status)){
|
||||
FilterResult filterResult=new FilterResult();
|
||||
filterResult.filter=altTextFilter;
|
||||
filterResult.keywordMatches=List.of();
|
||||
filters.add(filterResult);
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
public void updateAccountInfo(){
|
||||
AccountSessionManager.getInstance().updateSessionLocalInfo(this);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<Instance> getInstance() {
|
||||
|
@ -360,18 +310,4 @@ public class AccountSession{
|
|||
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
|
||||
.build();
|
||||
}
|
||||
|
||||
public String getDefaultAvatarUrl() {
|
||||
return getInstance()
|
||||
.map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png"))
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
public boolean isNotificationsMentionsOnly(){
|
||||
return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
|
||||
}
|
||||
|
||||
public void setNotificationsMentionsOnly(boolean mentionsOnly){
|
||||
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,11 +12,10 @@ import android.graphics.drawable.Icon;
|
|||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.ChooseAccountForComposeActivity;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
|
@ -35,8 +34,6 @@ import org.joinmastodon.android.model.EmojiCategory;
|
|||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.UnifiedPushHelper;
|
||||
import org.unifiedpush.android.connector.UnifiedPush;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
@ -72,6 +69,7 @@ public class AccountSessionManager{
|
|||
private HashMap<String, List<EmojiCategory>> customEmojis=new HashMap<>();
|
||||
private HashMap<String, Long> instancesLastUpdated=new HashMap<>();
|
||||
private HashMap<String, Instance> instances=new HashMap<>();
|
||||
private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
|
||||
private Instance authenticatingInstance;
|
||||
private Application authenticatingApp;
|
||||
private String lastActiveAccountID;
|
||||
|
@ -95,7 +93,6 @@ public class AccountSessionManager{
|
|||
|
||||
private AccountSessionManager(){
|
||||
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
|
||||
// This file should not be backed up, otherwise the app may start with accounts already logged in. See res/xml/backup_rules.xml
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
if(!file.exists())
|
||||
return;
|
||||
|
@ -110,12 +107,11 @@ public class AccountSessionManager{
|
|||
Log.e(TAG, "Error loading accounts", x);
|
||||
}
|
||||
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
||||
readInstanceInfo(domains);
|
||||
MastodonAPIController.runInBackground(()->readInstanceInfo(domains));
|
||||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
|
||||
Context context = MastodonApp.context;
|
||||
instances.put(instance.uri, instance);
|
||||
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
|
||||
sessions.put(session.getID(), session);
|
||||
|
@ -127,15 +123,8 @@ public class AccountSessionManager{
|
|||
wrapper.instance = instance;
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
|
||||
|
||||
updateMoreInstanceInfo(instance, AccountSessionManager.get(session.getID()).domain);
|
||||
if (UnifiedPushHelper.isUnifiedPushEnabled(context)) {
|
||||
UnifiedPush.register(
|
||||
context,
|
||||
session.getID(),
|
||||
null,
|
||||
session.app.vapidKey.replaceAll("=","")
|
||||
);
|
||||
} else if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
updateMoreInstanceInfo(instance, instance.uri);
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
maybeUpdateShortcuts();
|
||||
|
@ -216,17 +205,12 @@ public class AccountSessionManager{
|
|||
public void removeAccount(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
session.getCacheController().getListsFile().delete();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
MastodonApp.context.deleteSharedPreferences(id);
|
||||
}else{
|
||||
String dataDir=MastodonApp.context.getApplicationInfo().dataDir;
|
||||
if(dataDir!=null){
|
||||
File prefsDir=new File(dataDir, "shared_prefs");
|
||||
new File(prefsDir, id+".xml").delete();
|
||||
}
|
||||
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
|
||||
}
|
||||
sessions.remove(id);
|
||||
if(lastActiveAccountID.equals(id)){
|
||||
|
@ -248,6 +232,11 @@ public class AccountSessionManager{
|
|||
maybeUpdateShortcuts();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MastodonAPIController getUnauthenticatedApiController(){
|
||||
return unauthenticatedApiController;
|
||||
}
|
||||
|
||||
public void authenticate(Activity activity, Instance instance){
|
||||
authenticatingInstance=instance;
|
||||
new CreateOAuthApp()
|
||||
|
@ -325,7 +314,8 @@ public class AccountSessionManager{
|
|||
}
|
||||
}
|
||||
|
||||
/*package*/ void updateSessionLocalInfo(AccountSession session){
|
||||
|
||||
private void updateSessionLocalInfo(AccountSession session){
|
||||
new GetOwnAccount()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
|
@ -391,7 +381,7 @@ public class AccountSessionManager{
|
|||
public void onError(ErrorResponse errorResponse) {
|
||||
updateInstanceEmojis(instance, domain);
|
||||
}
|
||||
}).execNoAuth(domain);
|
||||
}).execNoAuth(instance.uri);
|
||||
}
|
||||
|
||||
private void updateInstanceEmojis(Instance instance, String domain){
|
||||
|
@ -485,19 +475,15 @@ public class AccountSessionManager{
|
|||
if(Build.VERSION.SDK_INT<26)
|
||||
return;
|
||||
ShortcutManager sm=MastodonApp.context.getSystemService(ShortcutManager.class);
|
||||
|
||||
Intent intent = new Intent(MastodonApp.context, ChooseAccountForComposeActivity.class)
|
||||
.setAction(Intent.ACTION_CHOOSER)
|
||||
.putExtra("compose", true);
|
||||
|
||||
// This was done so that the old shortcuts get updated to the new implementation.
|
||||
if((sm.getDynamicShortcuts().isEmpty() || sm.getDynamicShortcuts().get(0).getIntent() != intent || BuildConfig.DEBUG ) && !sessions.isEmpty()){
|
||||
if((sm.getDynamicShortcuts().isEmpty() || BuildConfig.DEBUG) && !sessions.isEmpty()){
|
||||
// There are no shortcuts, but there are accounts. Add a compose shortcut.
|
||||
ShortcutInfo info=new ShortcutInfo.Builder(MastodonApp.context, "compose")
|
||||
.setActivity(ComponentName.createRelative(MastodonApp.context, MainActivity.class.getName()))
|
||||
.setShortLabel(MastodonApp.context.getString(R.string.new_post))
|
||||
.setIcon(Icon.createWithResource(MastodonApp.context, R.mipmap.ic_shortcut_compose))
|
||||
.setIntent(intent)
|
||||
.setIntent(new Intent(MastodonApp.context, MainActivity.class)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.putExtra("compose", true))
|
||||
.build();
|
||||
sm.setDynamicShortcuts(Collections.singletonList(info));
|
||||
}else if(sessions.isEmpty()){
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class AccountAddedToListEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
public final Account account;
|
||||
|
||||
public AccountAddedToListEvent(String accountID, String listID, Account account){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
this.account=account;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
public class AccountRemovedFromListEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
public final String targetAccountID;
|
||||
|
||||
public AccountRemovedFromListEvent(String accountID, String listID, String targetAccountID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
this.targetAccountID=targetAccountID;
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.joinmastodon.android.model.EmojiReaction;
|
||||
import java.util.List;
|
||||
|
||||
public class EmojiReactionsUpdatedEvent{
|
||||
public final String id;
|
||||
public final List<EmojiReaction> reactions;
|
||||
public final boolean updateTextPadding;
|
||||
public RecyclerView.ViewHolder viewHolder;
|
||||
|
||||
public EmojiReactionsUpdatedEvent(String id, List<EmojiReaction> reactions, boolean updateTextPadding, RecyclerView.ViewHolder viewHolder){
|
||||
this.id=id;
|
||||
this.reactions=reactions;
|
||||
this.updateTextPadding=updateTextPadding;
|
||||
this.viewHolder=viewHolder;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
public class FinishListCreationFragmentEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
|
||||
public FinishListCreationFragmentEvent(String accountID, String listID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class ListCreatedEvent{
|
||||
public final String accountID;
|
||||
public final FollowList list;
|
||||
|
||||
public ListCreatedEvent(String accountID, FollowList list){
|
||||
this.accountID=accountID;
|
||||
this.list=list;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
public class ListDeletedEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
public class ListDeletedEvent {
|
||||
public final String id;
|
||||
|
||||
public ListDeletedEvent(String accountID, String listID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
public ListDeletedEvent(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
|
||||
public class ListUpdatedCreatedEvent {
|
||||
public final String id;
|
||||
public final String title;
|
||||
public final FollowList.RepliesPolicy repliesPolicy;
|
||||
public final ListTimeline.RepliesPolicy repliesPolicy;
|
||||
public final boolean exclusive;
|
||||
|
||||
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, FollowList.RepliesPolicy repliesPolicy) {
|
||||
public ListUpdatedCreatedEvent(String id, String title, boolean exclusive, ListTimeline.RepliesPolicy repliesPolicy) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.exclusive = exclusive;
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class ListUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final FollowList list;
|
||||
|
||||
public ListUpdatedEvent(String accountID, FollowList list){
|
||||
this.accountID=accountID;
|
||||
this.list=list;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
public class ReblogDeletedEvent{
|
||||
public final String statusID;
|
||||
public final String accountID;
|
||||
|
||||
public ReblogDeletedEvent(String statusID, String accountID){
|
||||
this.statusID=statusID;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
|
||||
public class ScheduledStatusDeletedEvent{
|
||||
public final String id;
|
||||
public final String accountID;
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.model.EmojiReaction;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class StatusCountersUpdatedEvent{
|
||||
public String id;
|
||||
public long favorites, reblogs, replies;
|
||||
public boolean favorited, reblogged, bookmarked, pinned;
|
||||
public List<EmojiReaction> reactions;
|
||||
public Status status;
|
||||
public RecyclerView.ViewHolder viewHolder;
|
||||
|
||||
public StatusCountersUpdatedEvent(Status s){
|
||||
this(s, null);
|
||||
}
|
||||
|
||||
public StatusCountersUpdatedEvent(Status s, RecyclerView.ViewHolder vh){
|
||||
id=s.id;
|
||||
status=s;
|
||||
favorites=s.favouritesCount;
|
||||
|
@ -18,5 +31,7 @@ public class StatusCountersUpdatedEvent{
|
|||
reblogged=s.reblogged;
|
||||
bookmarked=s.bookmarked;
|
||||
pinned=s.pinned;
|
||||
reactions=new ArrayList<>(s.reactions);
|
||||
viewHolder=vh;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,5 @@ public class StatusCreatedEvent{
|
|||
public StatusCreatedEvent(Status status, String accountID){
|
||||
this.status=status;
|
||||
this.accountID=accountID;
|
||||
status.fromStatusCreated=true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
public class StatusMuteChangedEvent{
|
||||
public String id;
|
||||
public boolean muted;
|
||||
public Status status;
|
||||
|
||||
public StatusMuteChangedEvent(Status s){
|
||||
id=s.id;
|
||||
muted=s.muted;
|
||||
status=s;
|
||||
}
|
||||
}
|
|
@ -9,16 +9,19 @@ 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.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
|
@ -52,14 +55,15 @@ public class AccountTimelineFragment extends StatusListFragment{
|
|||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAccountStatuses(user.id, getMaxID(), null, count, filter)
|
||||
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
boolean more=applyMaxID(result);
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
boolean empty=result.isEmpty();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
|
||||
onDataLoaded(result, more);
|
||||
onDataLoaded(result, !empty);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountLists;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AccountAddedToListEvent;
|
||||
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AddAccountToListsFragment extends BaseSettingsFragment<FollowList>{
|
||||
private Account account;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.add_user_to_list_title);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.get(accountID).getCacheController().getLists(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> allLists){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
loadAccountLists(allLists);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAccountLists(final List<FollowList> allLists){
|
||||
currentRequest=new GetAccountLists(account.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
Set<String> lists=result.stream().map(l->l.id).collect(Collectors.toSet());
|
||||
onDataLoaded(allLists.stream()
|
||||
.map(l->new CheckableListItem<>(l.title, null, CheckableListItem.Style.CHECKBOX, lists.contains(l.id),
|
||||
R.drawable.ic_list_alt_24px, AddAccountToListsFragment.this::onItemClick, l))
|
||||
.collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
TextView topText=new TextView(getActivity());
|
||||
topText.setTextAppearance(R.style.m3_body_medium);
|
||||
topText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||
topText.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
|
||||
topText.setText(getString(R.string.manage_user_lists, account.getDisplayUsername()));
|
||||
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(topText));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private void onItemClick(CheckableListItem<FollowList> item){
|
||||
boolean add=!item.checked;
|
||||
ResultlessMastodonAPIRequest req=add ? new AddAccountsToList(item.parentObject.id, Set.of(account.id)) : new RemoveAccountsFromList(item.parentObject.id, Set.of(account.id));
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
item.checked=add;
|
||||
rebindItem(item);
|
||||
if(add){
|
||||
E.post(new AccountAddedToListEvent(accountID, item.parentObject.id, account));
|
||||
}else{
|
||||
E.post(new AccountRemovedFromListEvent(accountID, item.parentObject.id, account.id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
|
@ -68,14 +68,14 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
|||
instanceUser.url = "https://"+session.domain+"/about";
|
||||
instanceUser.avatar = instanceUser.avatarStatic = instance.thumbnail;
|
||||
instanceUser.emojis = List.of();
|
||||
Status fakeStatus = a.toStatus(isInstanceIceshrimp());
|
||||
Status fakeStatus = a.toStatus();
|
||||
TextStatusDisplayItem textItem = new TextStatusDisplayItem(a.id, HtmlParser.parse(a.content, a.emojis, a.mentions, a.tags, accountID), this, fakeStatus, true);
|
||||
textItem.textSelectable = true;
|
||||
|
||||
List<StatusDisplayItem> items=new ArrayList<>();
|
||||
items.add(HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead));
|
||||
items.add(textItem);
|
||||
if(!isInstanceAkkoma() && !isInstanceIceshrimp()) items.add(new EmojiReactionsStatusDisplayItem(a.id, this, fakeStatus, accountID, false, true));
|
||||
if(!isInstanceAkkoma()) items.add(new EmojiReactionsStatusDisplayItem(a.id, this, fakeStatus, accountID, false, true));
|
||||
return items;
|
||||
}
|
||||
|
||||
|
@ -97,7 +97,7 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
|||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result){
|
||||
if(getActivity()==null) return;
|
||||
if (getActivity() == null) return;
|
||||
|
||||
// get unread items first
|
||||
List<Announcement> data = result.stream().filter(a -> !a.read).collect(toList());
|
||||
|
|
|
@ -1,176 +0,0 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseEditListFragment extends BaseSettingsFragment<Void>{
|
||||
protected FollowList followList;
|
||||
protected AvatarPileListItem<Void> membersItem;
|
||||
protected CheckableListItem<Void> exclusiveItem;
|
||||
protected FloatingHintEditTextLayout titleEditLayout;
|
||||
protected EditText titleEdit;
|
||||
protected Spinner showRepliesSpinner;
|
||||
private APIRequest<?> getMembersRequest;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
|
||||
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false);
|
||||
List<ListItem<Void>> items=new ArrayList<>();
|
||||
if(followList!=null){
|
||||
items.add(membersItem);
|
||||
}
|
||||
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList!=null && followList.exclusive, this::toggleCheckableItem);
|
||||
items.add(exclusiveItem);
|
||||
onDataLoaded(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(getMembersRequest!=null)
|
||||
getMembersRequest.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
LinearLayout topView=new LinearLayout(getActivity());
|
||||
topView.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.list_name);
|
||||
titleEditLayout.updateHint();
|
||||
if(followList!=null)
|
||||
titleEdit.setText(followList.title);
|
||||
topView.addView(titleEditLayout);
|
||||
|
||||
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
|
||||
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
|
||||
showRepliesLayout.setHint(R.string.list_show_replies_to);
|
||||
topView.addView(showRepliesLayout);
|
||||
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
|
||||
getString(R.string.list_replies_no_one),
|
||||
getString(R.string.list_replies_members),
|
||||
getString(R.string.list_replies_anyone)
|
||||
));
|
||||
showRepliesSpinner.setAdapter(spinnerAdapter);
|
||||
showRepliesSpinner.setSelection(switch(followList!=null ? followList.repliesPolicy : FollowList.RepliesPolicy.LIST){
|
||||
case FOLLOWED -> 2;
|
||||
case LIST -> 1;
|
||||
case NONE -> 0;
|
||||
});
|
||||
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
|
||||
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected void doDeleteList(){
|
||||
new DeleteList(followList.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
AccountSessionManager.get(accountID).getCacheController().deleteList(followList.id);
|
||||
E.post(new ListDeletedEvent(accountID, followList.id));
|
||||
Nav.finish(BaseEditListFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onMembersClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||
}
|
||||
|
||||
protected void loadMembers(){
|
||||
getMembersRequest=new GetListAccounts(followList.id, null, 3)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
getMembersRequest=null;
|
||||
membersItem.avatars=new ArrayList<>();
|
||||
for(int i=0;i<Math.min(3, result.size());i++){
|
||||
Account acc=result.get(i);
|
||||
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
|
||||
}
|
||||
rebindItem(membersItem);
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getMembersRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
protected FollowList.RepliesPolicy getSelectedRepliesPolicy(){
|
||||
return switch(showRepliesSpinner.getSelectedItemPosition()){
|
||||
case 0 -> FollowList.RepliesPolicy.NONE;
|
||||
case 1 -> FollowList.RepliesPolicy.LIST;
|
||||
case 2 -> FollowList.RepliesPolicy.FOLLOWED;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+showRepliesSpinner.getSelectedItemPosition());
|
||||
};
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import android.graphics.Rect;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
|
@ -24,27 +24,18 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.NonMutualPreReplySheet;
|
||||
import org.joinmastodon.android.ui.OldPostPreReplySheet;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.EmojiReactionsStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
|
@ -53,34 +44,33 @@ import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
|||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PreviewlessMediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.PreviewlessMediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
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.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
@ -100,10 +90,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected Rect tmpRect=new Rect();
|
||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
|
||||
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, PreviewlessMediaAttachmentViewController> previewlessAttachmentViewsPool=new TypedObjectPool<>(this::makeNewPreviewlessMediaAttachmentView);
|
||||
|
||||
protected boolean currentlyScrolling;
|
||||
protected String maxID;
|
||||
|
||||
public BaseStatusListFragment(){
|
||||
super(20);
|
||||
|
@ -145,7 +132,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
for(T s:items){
|
||||
displayItems.addAll(buildDisplayItems(s));
|
||||
}
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -167,13 +153,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
}
|
||||
if(notify)
|
||||
adapter.notifyItemRangeInserted(0, offset);
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
return offset;
|
||||
}
|
||||
|
||||
protected String getMaxID(){
|
||||
if(refreshing) return null;
|
||||
if(maxID!=null) return maxID;
|
||||
if(!preloadedData.isEmpty())
|
||||
return preloadedData.get(preloadedData.size()-1).getID();
|
||||
else if(!data.isEmpty())
|
||||
|
@ -182,12 +165,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
return null;
|
||||
}
|
||||
|
||||
protected boolean applyMaxID(List<Status> result){
|
||||
boolean empty=result.isEmpty();
|
||||
if(!empty) maxID=result.get(result.size()-1).id;
|
||||
return !empty;
|
||||
}
|
||||
|
||||
protected abstract List<StatusDisplayItem> buildDisplayItems(T s);
|
||||
protected abstract void addAccountToKnown(T s);
|
||||
|
||||
|
@ -224,7 +201,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
@Override
|
||||
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
||||
final Status status=_status.getContentStatus();
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
||||
private MediaAttachmentViewController transitioningHolder;
|
||||
|
||||
@Override
|
||||
|
@ -290,7 +267,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
currentPhotoViewer=null;
|
||||
gridHolder.itemView.setHasTransientState(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -302,80 +278,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
return gridHolder.getViewController(index);
|
||||
}
|
||||
});
|
||||
gridHolder.itemView.setHasTransientState(true);
|
||||
}
|
||||
|
||||
|
||||
public void openPreviewlessMediaPhotoViewer(String parentID, Status _status, int attachmentIndex, PreviewlessMediaGridStatusDisplayItem.Holder gridHolder){
|
||||
final Status status=_status.getContentStatus();
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||
private PreviewlessMediaAttachmentViewController transitioningHolder;
|
||||
|
||||
@Override
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
|
||||
PreviewlessMediaAttachmentViewController holder=findPhotoViewHolder(index);
|
||||
if(holder!=null && list!=null){
|
||||
transitioningHolder=holder;
|
||||
View view=transitioningHolder.inner;
|
||||
int[] pos={0, 0};
|
||||
view.getLocationOnScreen(pos);
|
||||
outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight());
|
||||
list.setClipChildren(false);
|
||||
gridHolder.setClipChildren(false);
|
||||
transitioningHolder.view.setElevation(1f);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
|
||||
View view=transitioningHolder.inner;
|
||||
view.setTranslationX(translateX);
|
||||
view.setTranslationY(translateY);
|
||||
view.setScaleX(scale);
|
||||
view.setScaleY(scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endPhotoViewTransition(){
|
||||
View view=transitioningHolder.inner;
|
||||
view.setTranslationX(0f);
|
||||
view.setTranslationY(0f);
|
||||
view.setScaleX(1f);
|
||||
view.setScaleY(1f);
|
||||
transitioningHolder.view.setElevation(0f);
|
||||
if(list!=null)
|
||||
list.setClipChildren(true);
|
||||
gridHolder.setClipChildren(true);
|
||||
transitioningHolder=null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Drawable getPhotoViewCurrentDrawable(int index){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
currentPhotoViewer=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissions(String[] permissions){
|
||||
requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
|
||||
}
|
||||
|
||||
private PreviewlessMediaAttachmentViewController findPhotoViewHolder(int index){
|
||||
return gridHolder.getViewController(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -450,14 +352,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
}
|
||||
});
|
||||
list.addItemDecoration(new StatusListItemDecoration());
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||
private Rect tmpRect=new Rect();
|
||||
@Override
|
||||
public void getSelectorBounds(View view, Rect outRect){
|
||||
if(list!=view.getParent()) return;
|
||||
boolean hasDescendant=false, hasAncestor=false, isWarning=false;
|
||||
int lastIndex=-1, firstIndex=-1;
|
||||
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
||||
int lastIndex = -1, firstIndex = -1;
|
||||
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
}else{
|
||||
|
@ -562,14 +462,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
protected void updatePoll(String itemID, Status status, Poll poll){
|
||||
status.poll=poll;
|
||||
int firstOptionIndex=-1, footerIndex=-1;
|
||||
int spoilerFirstOptionIndex=-1, spoilerFooterIndex=-1;
|
||||
SpoilerStatusDisplayItem spoilerItem=null;
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(item.parentID.equals(itemID)){
|
||||
if(item instanceof SpoilerStatusDisplayItem){
|
||||
spoilerItem=(SpoilerStatusDisplayItem) item;
|
||||
}else if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||
if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||
firstOptionIndex=i;
|
||||
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||
footerIndex=i;
|
||||
|
@ -578,39 +474,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// This is a temporary measure to deal with the app crashing when the poll isn't updated.
|
||||
// This is needed because of a possible id mismatch that screws with things
|
||||
if(firstOptionIndex==-1 || footerIndex==-1){
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(status.id.equals(itemID)){
|
||||
if(item instanceof SpoilerStatusDisplayItem){
|
||||
spoilerItem=(SpoilerStatusDisplayItem) item;
|
||||
}else if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||
firstOptionIndex=i;
|
||||
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||
footerIndex=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if(firstOptionIndex==-1 || footerIndex==-1)
|
||||
throw new IllegalStateException("Can't find all poll items in displayItems");
|
||||
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
||||
int prevSize=pollItems.size();
|
||||
if(spoilerItem!=null){
|
||||
spoilerFirstOptionIndex=spoilerItem.contentItems.indexOf(pollItems.get(0));
|
||||
spoilerFooterIndex=spoilerItem.contentItems.indexOf(pollItems.get(pollItems.size()-1));
|
||||
}
|
||||
pollItems.clear();
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems);
|
||||
if(spoilerItem!=null){
|
||||
spoilerItem.contentItems.subList(spoilerFirstOptionIndex, spoilerFooterIndex+1).clear();
|
||||
spoilerItem.contentItems.addAll(spoilerFirstOptionIndex, pollItems);
|
||||
}
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems, status);
|
||||
if(prevSize!=pollItems.size()){
|
||||
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
|
||||
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
|
||||
|
@ -622,8 +491,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){
|
||||
Poll poll=holder.getItem().poll;
|
||||
Poll.Option option=holder.getItem().option;
|
||||
// MEGALODON: always show vote button
|
||||
// if(poll.multiple){
|
||||
if(poll.multiple || GlobalUserPreferences.voteButtonForSingleChoice){
|
||||
if(poll.selectedOptions==null)
|
||||
poll.selectedOptions=new ArrayList<>();
|
||||
boolean optionContained=poll.selectedOptions.contains(option);
|
||||
|
@ -638,7 +506,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder vh=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(!poll.multiple && vh instanceof PollOptionStatusDisplayItem.Holder item){
|
||||
if(item!=holder) item.itemView.setSelected(false);
|
||||
if (item != holder) item.itemView.setSelected(false);
|
||||
}
|
||||
if(vh instanceof PollFooterStatusDisplayItem.Holder footer){
|
||||
if(footer.getItemID().equals(holder.getItemID())){
|
||||
|
@ -647,9 +515,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
}
|
||||
}
|
||||
}
|
||||
// }else{
|
||||
// submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
||||
// }
|
||||
}else{
|
||||
submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
|
||||
}
|
||||
}
|
||||
|
||||
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
|
||||
|
@ -657,33 +525,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
submitPollVote(holder.getItemID(), poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public void onPollViewResultsButtonClick(PollFooterStatusDisplayItem.Holder holder, boolean shown){
|
||||
int firstOptionIndex=-1, footerIndex=-1;
|
||||
int i=0;
|
||||
for(StatusDisplayItem item:displayItems){
|
||||
if(item.parentID.equals(holder.getItemID())){
|
||||
if(item instanceof PollOptionStatusDisplayItem && firstOptionIndex==-1){
|
||||
firstOptionIndex=i;
|
||||
}else if(item instanceof PollFooterStatusDisplayItem){
|
||||
footerIndex=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if(firstOptionIndex==-1 || footerIndex==-1)
|
||||
throw new IllegalStateException("Can't find all poll items in displayItems");
|
||||
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
|
||||
|
||||
for(StatusDisplayItem item:pollItems){
|
||||
if (item instanceof PollOptionStatusDisplayItem) {
|
||||
((PollOptionStatusDisplayItem) item).isAnimating=true;
|
||||
((PollOptionStatusDisplayItem) item).showResults=shown;
|
||||
adapter.notifyItemRangeChanged(firstOptionIndex, pollItems.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void submitPollVote(String parentID, String pollID, List<Integer> choices){
|
||||
if(refreshing)
|
||||
return;
|
||||
|
@ -705,145 +546,91 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
|
||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
boolean isForQuote=holder.getItem().isForQuote;
|
||||
toggleSpoiler(status, isForQuote, holder.getItemID());
|
||||
}
|
||||
|
||||
public void updateStatusWithQuote(DisplayItemsParent parent) {
|
||||
Pair<Integer, Integer> items=findAllItemsOfParent(parent);
|
||||
if (items==null)
|
||||
return;
|
||||
|
||||
// Only StatusListFragments/NotificationsListFragments can display status with quotes
|
||||
assert (this instanceof StatusListFragment) || (this instanceof NotificationsListFragment);
|
||||
List<StatusDisplayItem> oldItems = displayItems.subList(items.first, items.second+1);
|
||||
List<StatusDisplayItem> newItems=this.buildDisplayItems((T) parent);
|
||||
int prevSize=oldItems.size();
|
||||
oldItems.clear();
|
||||
displayItems.addAll(items.first, newItems);
|
||||
|
||||
// Update the cache
|
||||
final CacheController cache=AccountSessionManager.get(accountID).getCacheController();
|
||||
if (parent instanceof Status) {
|
||||
cache.updateStatus((Status) parent);
|
||||
} else if (parent instanceof Notification) {
|
||||
cache.updateNotification((Notification) parent);
|
||||
}
|
||||
|
||||
adapter.notifyItemRangeRemoved(items.first, prevSize);
|
||||
adapter.notifyItemRangeInserted(items.first, newItems.size());
|
||||
}
|
||||
|
||||
public void removeStatus(DisplayItemsParent parent) {
|
||||
Pair<Integer, Integer> items=findAllItemsOfParent(parent);
|
||||
if (items==null)
|
||||
return;
|
||||
|
||||
List<StatusDisplayItem> statusDisplayItems = displayItems.subList(items.first, items.second+1);
|
||||
int prevSize=statusDisplayItems.size();
|
||||
statusDisplayItems.clear();
|
||||
adapter.notifyItemRangeRemoved(items.first, prevSize);
|
||||
toggleSpoiler(status, holder.getItemID());
|
||||
}
|
||||
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
|
||||
Status status = holder.getItem().status;
|
||||
if(holder.getItem().hasVisibilityToggle) holder.animateVisibilityToggle(false);
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if(mediaGrid!=null){
|
||||
if(!status.sensitiveRevealed) mediaGrid.revealSensitive();
|
||||
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
|
||||
if (mediaGrid != null) {
|
||||
if (!status.sensitiveRevealed) mediaGrid.revealSensitive();
|
||||
else mediaGrid.hideSensitive();
|
||||
}else{
|
||||
status.sensitiveRevealed=false;
|
||||
notifyItemChangedAfter(holder.getItem(), MediaGridStatusDisplayItem.class);
|
||||
} else {
|
||||
// media grid's methods normally change the status' state - we still want to be able
|
||||
// to do this if the media grid is not bound, tho - so, doing it ourselves here
|
||||
status.sensitiveRevealed = !status.sensitiveRevealed;
|
||||
}
|
||||
holder.rebind();
|
||||
}
|
||||
|
||||
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null && header.getItem().hasVisibilityToggle) header.animateVisibilityToggle(true);
|
||||
else notifyItemChangedBefore(holder.getItem(), HeaderStatusDisplayItem.class);
|
||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if(header != null) header.rebind();
|
||||
}
|
||||
|
||||
protected void toggleSpoiler(Status status, boolean isForQuote, String itemID){
|
||||
protected void toggleSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
|
||||
status.sensitiveRevealed = false;
|
||||
|
||||
List<SpoilerStatusDisplayItem.Holder> spoilers=findAllHoldersOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
SpoilerStatusDisplayItem.Holder spoiler=spoilers.size() > 1 && isForQuote ? spoilers.get(1) : spoilers.get(0);
|
||||
if(spoiler!=null) spoiler.rebind();
|
||||
else notifyItemChanged(itemID, SpoilerStatusDisplayItem.class);
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(spoiler.getItem());
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null)
|
||||
spoiler.rebind();
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
||||
|
||||
int index=displayItems.indexOf(spoilerItem);
|
||||
if(status.spoilerRevealed){
|
||||
displayItems.addAll(index+1, spoilerItem.contentItems);
|
||||
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
|
||||
}else{
|
||||
if(spoilers.size()>1 && !isForQuote && status.quote.spoilerRevealed)
|
||||
toggleSpoiler(status.quote, true, itemID);
|
||||
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
|
||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||
}
|
||||
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null)
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null) header.rebind();
|
||||
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
||||
if(header!=null)
|
||||
header.rebind();
|
||||
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
|
||||
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable, boolean isForQuote) {
|
||||
Status s=holder.getItem().status;
|
||||
if(s.textExpandable!=expandable && list!=null) {
|
||||
s.textExpandable=expandable;
|
||||
List<HeaderStatusDisplayItem.Holder> headers=findAllHoldersOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if(headers!=null && !headers.isEmpty()){
|
||||
HeaderStatusDisplayItem.Holder header=headers.size() > 1 && isForQuote ? headers.get(1) : headers.get(0);
|
||||
if(header!=null) header.bindCollapseButton();
|
||||
}
|
||||
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
|
||||
if (holder.getItem().status.textExpandable != expandable && list != null) {
|
||||
holder.getItem().status.textExpandable = expandable;
|
||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if (header != null) header.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
public void onToggleExpanded(Status status, boolean isForQuote, String itemID) {
|
||||
public void onToggleExpanded(Status status, String itemID) {
|
||||
status.textExpanded = !status.textExpanded;
|
||||
// TODO: simplify this to a single case
|
||||
if(!isForQuote)
|
||||
// using the adapter directly to update the item does not work for non-quoted texts
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
else{
|
||||
List<TextStatusDisplayItem.Holder> textItems=findAllHoldersOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
TextStatusDisplayItem.Holder text=textItems.size()>1 ? textItems.get(1) : textItems.get(0);
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
|
||||
}
|
||||
List<HeaderStatusDisplayItem.Holder> headers=findAllHoldersOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if (headers.isEmpty())
|
||||
return;
|
||||
HeaderStatusDisplayItem.Holder header=headers.size() > 1 && isForQuote ? headers.get(1) : headers.get(0);
|
||||
if(header!=null) header.animateExpandToggle();
|
||||
else notifyItemChanged(itemID, HeaderStatusDisplayItem.class);
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if (text != null) text.rebind();
|
||||
if (header != null) header.rebind();
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item, boolean downwards){}
|
||||
public void updateEmojiReactions(Status status, String itemID){
|
||||
EmojiReactionsStatusDisplayItem.Holder reactions=findHolderOfType(itemID, EmojiReactionsStatusDisplayItem.Holder.class);
|
||||
if(reactions != null){
|
||||
reactions.getItem().status.reactions.clear();
|
||||
reactions.getItem().status.reactions.addAll(status.reactions);
|
||||
reactions.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||
|
||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||
WarningFilteredStatusDisplayItem filterItem=findItemOfType(warning.getItemID(), WarningFilteredStatusDisplayItem.class);
|
||||
int startPos=displayItems.indexOf(filterItem);
|
||||
int startPos = warning.getAbsoluteAdapterPosition();
|
||||
displayItems.remove(startPos);
|
||||
displayItems.addAll(startPos, warning.filteredItems);
|
||||
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
||||
if (startPos == 0) scrollToTop();
|
||||
warning.getItem().status.filterRevealed = true;
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
|
||||
public void onFavoriteChanged(Status status, String itemID) {
|
||||
FooterStatusDisplayItem.Holder footer=findHolderOfType(itemID, FooterStatusDisplayItem.Holder.class);
|
||||
if(footer!=null){
|
||||
footer.getItem().status=status;
|
||||
footer.onFavoriteClick();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -860,9 +647,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
}
|
||||
|
||||
protected void loadRelationships(Set<String> ids){
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
// TODO somehow manage these and cancel outstanding requests on refresh
|
||||
|
@ -894,61 +678,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this as a fallback if findHolderOfType fails to find the ViewHolder.
|
||||
* It might still be bound but off-screen and therefore not a child of the RecyclerView -
|
||||
* resulting in the ViewHolder displaying an outdated state once scrolled back into view.
|
||||
*/
|
||||
protected <I extends StatusDisplayItem> int notifyItemChanged(String id, Class<I> type){
|
||||
boolean encounteredParent=false;
|
||||
for(int i=0; i<displayItems.size(); i++){
|
||||
StatusDisplayItem item=displayItems.get(i);
|
||||
boolean idEquals=id.equals(item.parentID);
|
||||
if(!encounteredParent && idEquals) encounteredParent=true; // reached top of the parent
|
||||
else if(encounteredParent && !idEquals) break; // passed by bottom of the parent. man muss ja wissen wann schluss is
|
||||
if(idEquals && type.isInstance(item)){
|
||||
adapter.notifyItemChanged(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected <I extends StatusDisplayItem> int notifyItemChangedAfter(StatusDisplayItem afterThis, Class<I> type){
|
||||
int startIndex=displayItems.indexOf(afterThis);
|
||||
if(startIndex == -1) throw new IllegalStateException("notifyItemChangedAfter didn't find the passed StatusDisplayItem");
|
||||
String parentID=afterThis.parentID;
|
||||
for(int i=startIndex; i<displayItems.size(); i++){
|
||||
StatusDisplayItem item=displayItems.get(i);
|
||||
if(!parentID.equals(item.parentID)) break; // didn't find anything
|
||||
if(type.isInstance(item)){
|
||||
// found it
|
||||
adapter.notifyItemChanged(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected <I extends StatusDisplayItem> int notifyItemChangedBefore(StatusDisplayItem beforeThis, Class<I> type){
|
||||
int startIndex=displayItems.indexOf(beforeThis);
|
||||
if(startIndex == -1) throw new IllegalStateException("notifyItemChangedBefore didn't find the passed StatusDisplayItem");
|
||||
String parentID=beforeThis.parentID;
|
||||
for(int i=startIndex; i>=0; i--){
|
||||
StatusDisplayItem item=displayItems.get(i);
|
||||
if(!parentID.equals(item.parentID)) break; // didn't find anything
|
||||
if(type.isInstance(item)){
|
||||
// found it
|
||||
adapter.notifyItemChanged(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> H findHolderOfType(String id, Class<H> type){
|
||||
for(int i=0; i<list.getChildCount(); i++){
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof StatusDisplayItem.Holder<?> itemHolder && itemHolder.getItemID().equals(id) && type.isInstance(holder))
|
||||
return type.cast(holder);
|
||||
|
@ -956,23 +688,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Pair<Integer, Integer> findAllItemsOfParent(DisplayItemsParent parent){
|
||||
int startIndex=-1;
|
||||
int endIndex=-1;
|
||||
for(int i=0; i<displayItems.size(); i++){
|
||||
StatusDisplayItem item = displayItems.get(i);
|
||||
if(item.parentID.equals(parent.getID())) {
|
||||
startIndex= startIndex==-1 ? i : startIndex;
|
||||
endIndex=i;
|
||||
}
|
||||
}
|
||||
|
||||
if(startIndex==-1 || endIndex==-1)
|
||||
return null;
|
||||
return Pair.create(startIndex, endIndex);
|
||||
}
|
||||
|
||||
protected <I extends StatusDisplayItem, H extends StatusDisplayItem.Holder<I>> List<H> findAllHoldersOfType(String id, Class<H> type){
|
||||
ArrayList<H> holders=new ArrayList<>();
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
|
@ -1053,116 +768,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
return new MediaAttachmentViewController(getActivity(), type);
|
||||
}
|
||||
|
||||
private PreviewlessMediaAttachmentViewController makeNewPreviewlessMediaAttachmentView(MediaGridStatusDisplayItem.GridItemType type){
|
||||
return new PreviewlessMediaAttachmentViewController(getActivity(), type);
|
||||
}
|
||||
|
||||
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> getAttachmentViewsPool(){
|
||||
return attachmentViewsPool;
|
||||
}
|
||||
|
||||
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, PreviewlessMediaAttachmentViewController> getPreviewlessAttachmentViewsPool(){
|
||||
return previewlessAttachmentViewsPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProvideAssistContent(AssistContent assistContent) {
|
||||
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
|
||||
}
|
||||
|
||||
public void togglePostTranslation(Status status, String itemID){
|
||||
switch(status.translationState){
|
||||
case LOADING -> {
|
||||
return;
|
||||
}
|
||||
case SHOWN -> {
|
||||
status.translationState=Status.TranslationState.HIDDEN;
|
||||
}
|
||||
case HIDDEN -> {
|
||||
if(status.translation!=null){
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
}else{
|
||||
status.translationState=Status.TranslationState.LOADING;
|
||||
Consumer<Translation> successCallback=(result)->{
|
||||
status.translation=result;
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
updateTranslation(itemID);
|
||||
};
|
||||
MastodonAPIRequest<?> req=isInstanceAkkoma()
|
||||
? new AkkomaTranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(AkkomaTranslation result){
|
||||
if(getActivity()!=null) successCallback.accept(result.toTranslation());
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()!=null) translationCallbackError(status, itemID);
|
||||
}
|
||||
})
|
||||
: new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Translation result){
|
||||
if(getActivity()!=null) successCallback.accept(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()!=null) translationCallbackError(status, itemID);
|
||||
}
|
||||
});
|
||||
|
||||
// 1 minute
|
||||
req.setTimeout(60000).exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTranslation(itemID);
|
||||
}
|
||||
|
||||
private void translationCallbackError(Status status, String itemID) {
|
||||
status.translationState=Status.TranslationState.HIDDEN;
|
||||
updateTranslation(itemID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.translation_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updateTranslation(String itemID) {
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
text.updateTranslation(true);
|
||||
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
|
||||
}else{
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
}
|
||||
|
||||
if(isInstanceAkkoma())
|
||||
return;
|
||||
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null){
|
||||
spoiler.rebind();
|
||||
}
|
||||
|
||||
MediaGridStatusDisplayItem.Holder media=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
|
||||
if (media!=null) {
|
||||
media.rebind();
|
||||
}
|
||||
|
||||
PreviewlessMediaGridStatusDisplayItem.Holder previewLessMedia=findHolderOfType(itemID, PreviewlessMediaGridStatusDisplayItem.Holder.class);
|
||||
if (previewLessMedia!=null) {
|
||||
previewLessMedia.rebind();
|
||||
}
|
||||
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item){
|
||||
item.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
displayItems.clear();
|
||||
for(T item:data){
|
||||
|
@ -1171,26 +785,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void maybeShowPreReplySheet(Status status, Runnable proceed){
|
||||
Relationship rel=getRelationship(status.account.id);
|
||||
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
|
||||
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
|
||||
new NonMutualPreReplySheet(getActivity(), notAgain->{
|
||||
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
|
||||
proceed.run();
|
||||
}, status.account, accountID).show();
|
||||
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
|
||||
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
|
||||
new OldPostPreReplySheet(getActivity(), notAgain->{
|
||||
if(notAgain)
|
||||
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
|
||||
proceed.run();
|
||||
}, status).show();
|
||||
}else{
|
||||
proceed.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
@Override
|
||||
|
@ -1198,7 +792,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
if(getContext()==null) return;
|
||||
super.onDataLoaded(d, more);
|
||||
// more available, but the page isn't even full yet? seems wrong, let's load some more
|
||||
if(more && data.size() < itemsPerPage){
|
||||
if(more && d.size() < itemsPerPage){
|
||||
preloader.onScrolledToLastItem();
|
||||
}
|
||||
}
|
||||
|
@ -1254,7 +848,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
{
|
||||
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), GlobalUserPreferences.showDividers ? R.attr.colorM3OutlineVariant : R.attr.colorM3Surface));
|
||||
dividerPaint.setStyle(Paint.Style.STROKE);
|
||||
dividerPaint.setStrokeWidth(V.dp(1f));
|
||||
dividerPaint.setStrokeWidth(V.dp(0.5f));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -28,7 +28,7 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
|||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
if (getActivity() == null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
|
|
@ -7,9 +7,6 @@ import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDra
|
|||
|
||||
import android.Manifest;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.DatePickerDialog;
|
||||
|
@ -65,7 +62,6 @@ import com.twitter.twittertext.TwitterTextEmojiRegex;
|
|||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.TweakedFileProvider;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
|
||||
|
@ -79,7 +75,7 @@ import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
|||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
|
@ -99,7 +95,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
|
|||
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.utils.Tracking;
|
||||
import org.joinmastodon.android.utils.FileProvider;
|
||||
import org.joinmastodon.android.utils.TransferSpeedTracker;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ComposeAutocompleteViewController;
|
||||
|
@ -127,11 +123,11 @@ import java.time.format.DateTimeFormatter;
|
|||
import java.time.format.FormatStyle;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -139,14 +135,12 @@ import java.util.stream.Collectors;
|
|||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID, CustomTransitionsFragment {
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
|
@ -171,7 +165,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
|
||||
public LinearLayout mainLayout;
|
||||
private SizeListenerLinearLayout contentView;
|
||||
private TextView selfName, selfUsername, selfExtraText, extraText;
|
||||
private TextView selfName, selfUsername, selfExtraText, extraText, pronouns;
|
||||
private ImageView selfAvatar;
|
||||
private Account self;
|
||||
private String instanceDomain;
|
||||
|
@ -182,7 +176,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
private int charCount, charLimit, trimmedCharCount;
|
||||
|
||||
private Button publishButton, languageButton, scheduleTimeBtn;
|
||||
private PopupMenu contentTypePopup, visibilityPopup, draftOptionsPopup;
|
||||
private PopupMenu languagePopup, contentTypePopup, visibilityPopup, draftOptionsPopup;
|
||||
private ImageButton publishButtonRelocated, mediaBtn, pollBtn, emojiBtn, spoilerBtn, draftsBtn, scheduleDraftDismiss, contentTypeBtn;
|
||||
private View sensitiveBtn;
|
||||
private TextView replyText;
|
||||
|
@ -221,7 +215,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
public Instance instance;
|
||||
|
||||
public Status editingStatus;
|
||||
public ScheduledStatus scheduledStatus;
|
||||
private ScheduledStatus scheduledStatus;
|
||||
private boolean redraftStatus;
|
||||
|
||||
private Uri photoUri;
|
||||
|
@ -318,7 +312,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
creatingView=true;
|
||||
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), accountID, customEmojis, instanceDomain);
|
||||
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain, getAccountID());
|
||||
emojiKeyboard.setListener(new CustomEmojiPopupKeyboard.Listener(){
|
||||
@Override
|
||||
public void onEmojiSelected(Emoji emoji){
|
||||
|
@ -370,7 +364,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
selfUsername=view.findViewById(R.id.self_username);
|
||||
selfAvatar=view.findViewById(R.id.self_avatar);
|
||||
selfExtraText=view.findViewById(R.id.self_extra_text);
|
||||
HtmlParser.setTextWithCustomEmoji(selfName, self.getDisplayName(), self.emojis);
|
||||
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
|
||||
selfUsername.setText('@'+self.username+'@'+instanceDomain);
|
||||
if(self.avatar!=null)
|
||||
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
||||
|
@ -471,7 +465,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
hasSpoiler=true;
|
||||
spoilerWrap.setVisibility(View.VISIBLE);
|
||||
spoilerBtn.setSelected(true);
|
||||
}else if(editingStatus!=null && editingStatus.hasSpoiler()){
|
||||
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
|
||||
hasSpoiler=true;
|
||||
spoilerWrap.setVisibility(View.VISIBLE);
|
||||
spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText));
|
||||
|
@ -513,8 +507,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
|
||||
int typeIndex=contentType.ordinal();
|
||||
if (contentTypePopup.getMenu().findItem(typeIndex) != null)
|
||||
contentTypePopup.getMenu().findItem(typeIndex).setChecked(true);
|
||||
contentTypePopup.getMenu().findItem(typeIndex).setChecked(true);
|
||||
contentTypeBtn.setSelected(typeIndex != ContentType.UNSPECIFIED.ordinal() && typeIndex != ContentType.PLAIN.ordinal());
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
|
@ -534,7 +527,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
public void onLaunchAccountSearch(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
}
|
||||
});
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
|
@ -692,6 +685,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
});
|
||||
View originalPost=view.findViewById(R.id.original_post);
|
||||
extraText=view.findViewById(R.id.extra_text);
|
||||
pronouns=view.findViewById(R.id.pronouns);
|
||||
originalPost.setVisibility(View.VISIBLE);
|
||||
originalPost.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
|
@ -731,7 +725,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
moreBtn.setBackground(null);
|
||||
|
||||
TextView name = view.findViewById(R.id.name);
|
||||
name.setText(HtmlParser.parseCustomEmoji(status.account.getDisplayName(), status.account.emojis));
|
||||
name.setText(HtmlParser.parseCustomEmoji(status.account.displayName, status.account.emojis));
|
||||
UiUtils.loadCustomEmojiInTextView(name);
|
||||
|
||||
String time = status==null || status.editedAt==null
|
||||
|
@ -743,7 +737,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
view.findViewById(R.id.time).setVisibility(time==null ? View.GONE : View.VISIBLE);
|
||||
if(time!=null) ((TextView) view.findViewById(R.id.time)).setText(time);
|
||||
|
||||
if (status.hasSpoiler()) {
|
||||
if (status.spoilerText != null && !status.spoilerText.isBlank()) {
|
||||
TextView replyToSpoiler = view.findViewById(R.id.reply_to_spoiler);
|
||||
replyToSpoiler.setVisibility(View.VISIBLE);
|
||||
replyToSpoiler.setText(status.spoilerText);
|
||||
|
@ -765,8 +759,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
|
||||
}
|
||||
|
||||
replyText.setText(HtmlParser.parseCustomEmoji(getString(quote!=null? R.string.sk_quoting_user : R.string.in_reply_to, status.account.getDisplayName()), status.account.emojis));
|
||||
UiUtils.loadCustomEmojiInTextView(replyText);
|
||||
replyText.setText(getString(quote!=null? R.string.sk_quoting_user : R.string.in_reply_to, status.account.displayName));
|
||||
int visibilityNameRes = switch (status.visibility) {
|
||||
case PUBLIC -> R.string.visibility_public;
|
||||
case UNLISTED -> R.string.sk_visibility_unlisted;
|
||||
|
@ -774,7 +767,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
case DIRECT -> R.string.visibility_private;
|
||||
case LOCAL -> R.string.sk_local_only;
|
||||
};
|
||||
replyText.setContentDescription(getString(R.string.in_reply_to, status.account.getDisplayName()) + ", " + getString(visibilityNameRes));
|
||||
replyText.setContentDescription(getString(R.string.in_reply_to, status.account.displayName) + ", " + getString(visibilityNameRes));
|
||||
replyText.setOnClickListener(v->{
|
||||
scrollView.smoothScrollTo(0, 0);
|
||||
});
|
||||
|
@ -787,7 +780,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
|
||||
if(!status.account.id.equals(ownID))
|
||||
mentions.add('@'+status.account.acct);
|
||||
if(GlobalUserPreferences.mentionRebloggerAutomatically && status.rebloggedBy != null && !status.rebloggedBy.id.equals(ownID))
|
||||
if(status.rebloggedBy != null && GlobalUserPreferences.mentionRebloggerAutomatically)
|
||||
mentions.add('@'+status.rebloggedBy.acct);
|
||||
for(Mention mention:status.mentions){
|
||||
if(mention.id.equals(ownID))
|
||||
|
@ -808,7 +801,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS
|
||||
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
|
||||
&& !status.spoilerText.startsWith("re: ") ? "re: " : "";
|
||||
spoilerEdit.setText(prefix + status.spoilerText);
|
||||
spoilerEdit.setText(prefix + replyTo.spoilerText);
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language);
|
||||
|
@ -869,7 +862,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu);
|
||||
|
@ -897,22 +889,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
charCounter.setText(String.valueOf(charLimit));
|
||||
}
|
||||
|
||||
// draftsBtn=wrap.findViewById(R.id.drafts_btn);
|
||||
draftOptionsPopup=new PopupMenu(getContext(), draftsBtn);
|
||||
// draftsBtn = wrap.findViewById(R.id.drafts_btn);
|
||||
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
|
||||
draftOptionsPopup.inflate(R.menu.compose_more);
|
||||
Menu draftOptionsMenu=draftOptionsPopup.getMenu();
|
||||
draftMenuItem=draftOptionsMenu.findItem(R.id.draft);
|
||||
undraftMenuItem=draftOptionsMenu.findItem(R.id.undraft);
|
||||
scheduleMenuItem=draftOptionsMenu.findItem(R.id.schedule);
|
||||
unscheduleMenuItem=draftOptionsMenu.findItem(R.id.unschedule);
|
||||
draftOptionsMenu.findItem(R.id.preview).setVisible(isInstanceAkkoma());
|
||||
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
|
||||
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
|
||||
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
|
||||
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
|
||||
draftOptionsPopup.setOnMenuItemClickListener(i->{
|
||||
int id=i.getItemId();
|
||||
if(id==R.id.draft) updateScheduledAt(getDraftInstant());
|
||||
else if(id==R.id.schedule) pickScheduledDateTime();
|
||||
else if(id==R.id.unschedule || id==R.id.undraft) updateScheduledAt(null);
|
||||
else if(id==R.id.drafts) navigateToUnsentPosts();
|
||||
else if(id==R.id.preview) publish(true);
|
||||
int id = i.getItemId();
|
||||
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
|
||||
else if (id == R.id.schedule) pickScheduledDateTime();
|
||||
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
|
||||
else navigateToUnsentPosts();
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
|
||||
|
@ -920,40 +909,23 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
|
||||
languageButton = wrap.findViewById(R.id.language_btn);
|
||||
languageButton.setOnClickListener(v->showLanguageAlert());
|
||||
languageButton.setOnLongClickListener(v->{
|
||||
if(!getLocalPrefs().bottomEncoding){
|
||||
getLocalPrefs().bottomEncoding=true;
|
||||
getLocalPrefs().save();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if(instance.isIceshrimpJs())
|
||||
languageButton.setVisibility(View.GONE); // hide language selector on Iceshrimp-JS because the feature is not supported
|
||||
|
||||
if (!GlobalUserPreferences.relocatePublishButton)
|
||||
publishButton.post(()->publishButton.setMinimumWidth(publishButton.getWidth()));
|
||||
if(GlobalUserPreferences.relocatePublishButton){
|
||||
publishButtonRelocated.setOnClickListener(v -> {
|
||||
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
|
||||
checkAltTextsAndPublish();
|
||||
else
|
||||
publish();
|
||||
});
|
||||
} else {
|
||||
publishButton.setOnClickListener(v -> {
|
||||
if(GlobalUserPreferences.altTextReminders && editingStatus==null)
|
||||
checkAltTextsAndPublish();
|
||||
else
|
||||
publish();
|
||||
});
|
||||
}
|
||||
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setOnClickListener(v->{
|
||||
Consumer<Boolean> draftCheckComplete=(isDraft)->{
|
||||
if(GlobalUserPreferences.altTextReminders && !isDraft) checkAltTextsAndPublish();
|
||||
else publish();
|
||||
};
|
||||
|
||||
boolean isAlreadyDraft=scheduledAt!=null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT);
|
||||
if(editingStatus!=null && scheduledAt!=null && isAlreadyDraft) {
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_save_draft)
|
||||
.setMessage(R.string.sk_save_draft_message)
|
||||
.setPositiveButton(R.string.save, (d, w)->draftCheckComplete.accept(isAlreadyDraft))
|
||||
.setNegativeButton(R.string.publish, (d, w)->{
|
||||
updateScheduledAt(null);
|
||||
draftCheckComplete.accept(false);
|
||||
})
|
||||
.show();
|
||||
}else{
|
||||
draftCheckComplete.accept(isAlreadyDraft);
|
||||
}
|
||||
});
|
||||
draftsBtn.setOnClickListener(v-> draftOptionsPopup.show());
|
||||
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
|
||||
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
|
||||
|
@ -964,8 +936,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
? languageResolver.fromOrFallback(prefs.postingDefaultLanguage)
|
||||
: languageResolver.getDefault());
|
||||
|
||||
if(isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||
if(isInstancePixelfed() || (editingStatus!=null && !redraftStatus)) {
|
||||
if (isInstancePixelfed()) spoilerBtn.setVisibility(View.GONE);
|
||||
if (isInstancePixelfed() || (editingStatus != null && scheduledStatus == null)) {
|
||||
// editing an already published post
|
||||
draftsBtn.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -1055,12 +1027,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
public void updatePublishButtonState(){
|
||||
uuid=null;
|
||||
if(GlobalUserPreferences.relocatePublishButton && publishButtonRelocated != null){
|
||||
publishButtonRelocated.setEnabled(((!isInstancePixelfed() || replyTo != null) || !mediaViewController.isEmpty()) && (trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1));
|
||||
publishButtonRelocated.setEnabled((!isInstancePixelfed() || !mediaViewController.isEmpty()) && (trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1));
|
||||
}
|
||||
|
||||
if(publishButton==null)
|
||||
return;
|
||||
publishButton.setEnabled(((!isInstancePixelfed() || replyTo != null) || !mediaViewController.isEmpty()) && (trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1));
|
||||
publishButton.setEnabled((!isInstancePixelfed() || !mediaViewController.isEmpty()) && (trimmedCharCount>0 || !mediaViewController.isEmpty()) && charCount<=charLimit && mediaViewController.getNonDoneAttachmentCount()==0 && (pollViewController.isEmpty() || pollViewController.getNonEmptyOptionsCount()>1));
|
||||
}
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
|
@ -1096,7 +1068,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
|
||||
@Override
|
||||
protected int getNavigationIconDrawableResource(){
|
||||
return R.drawable.ic_fluent_dismiss_24_regular;
|
||||
return R.drawable.ic_baseline_close_24;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1155,10 +1127,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
|
||||
private void publish(){
|
||||
publish(false);
|
||||
}
|
||||
|
||||
private void publish(boolean preview){
|
||||
sendingOverlay=new View(getActivity());
|
||||
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
|
||||
overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
|
||||
|
@ -1169,27 +1137,31 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
overlayParams.token=mainEditText.getWindowToken();
|
||||
wm.addView(sendingOverlay, overlayParams);
|
||||
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
|
||||
if(GlobalUserPreferences.relocatePublishButton){
|
||||
publishButtonRelocated.setEnabled(false);
|
||||
} else {
|
||||
publishButton.setEnabled(false);
|
||||
}
|
||||
|
||||
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
||||
|
||||
mediaViewController.saveAltTextsBeforePublishing(
|
||||
()->actuallyPublish(preview),
|
||||
this::handlePublishError);
|
||||
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
|
||||
}
|
||||
|
||||
private void actuallyPublish(boolean preview){
|
||||
private void actuallyPublish(){
|
||||
actuallyPublish(false);
|
||||
}
|
||||
private void actuallyPublish(boolean force){
|
||||
String text=mainEditText.getText().toString();
|
||||
if(GlobalUserPreferences.removeTrackingParams)
|
||||
text=Tracking.cleanUrlsInText(text);
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
if("bottom".equals(postLang.encoding)){
|
||||
text=new StatusTextEncoder(Bottom::encode).encode(text);
|
||||
req.spoilerText="bottom-encoded emoji spam";
|
||||
if ("bottom".equals(postLang.encoding)) {
|
||||
text = new StatusTextEncoder(Bottom::encode).encode(text);
|
||||
req.spoilerText = "bottom-encoded emoji spam";
|
||||
}
|
||||
if(localOnly &&
|
||||
if (localOnly &&
|
||||
AccountSessionManager.get(accountID).getLocalPreferences().glitchInstance &&
|
||||
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()){
|
||||
text+=" "+GLITCH_LOCAL_ONLY_SUFFIX;
|
||||
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) {
|
||||
text += " " + GLITCH_LOCAL_ONLY_SUFFIX;
|
||||
}
|
||||
req.status=text;
|
||||
req.localOnly=localOnly;
|
||||
|
@ -1197,12 +1169,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
req.sensitive=sensitive;
|
||||
req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType;
|
||||
req.scheduledAt=scheduledAt;
|
||||
req.preview=preview;
|
||||
if(!mediaViewController.isEmpty()){
|
||||
req.mediaIds=mediaViewController.getAttachmentIDs();
|
||||
if(editingStatus != null){
|
||||
req.mediaAttributes=mediaViewController.getAttachmentAttributes();
|
||||
}
|
||||
}
|
||||
// ask whether to publish now when editing an existing draft
|
||||
if (!force && editingStatus != null && scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_save_draft)
|
||||
.setMessage(R.string.sk_save_draft_message)
|
||||
.setPositiveButton(R.string.save, (d, w) -> actuallyPublish(true))
|
||||
.setNegativeButton(R.string.publish, (d, w) -> {
|
||||
updateScheduledAt(null);
|
||||
actuallyPublish();
|
||||
})
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
|
||||
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
|
||||
|
@ -1225,12 +1206,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
Callback<Status> resCallback=new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
if(preview){
|
||||
openPreview(result);
|
||||
return;
|
||||
}
|
||||
|
||||
maybeDeleteScheduledPost(()->{
|
||||
maybeDeleteScheduledPost(() -> {
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
if(editingStatus==null || redraftStatus){
|
||||
|
@ -1252,10 +1228,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
E.post(new StatusUpdatedEvent(editedStatus));
|
||||
}
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()){
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
||||
Nav.finish(ComposeFragment.this);
|
||||
}
|
||||
if(getArguments().getBoolean("navigateToStatus", false)){
|
||||
if (getArguments().getBoolean("navigateToStatus", false)) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
|
@ -1271,11 +1247,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
};
|
||||
|
||||
if(editingStatus!=null && !redraftStatus && !preview){
|
||||
if(editingStatus!=null && !redraftStatus){
|
||||
new EditStatus(req, editingStatus.id)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
}else if(req.scheduledAt == null || preview){
|
||||
}else if(req.scheduledAt == null){
|
||||
new CreateStatus(req, uuid)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
|
@ -1305,7 +1281,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
.setPositiveButton(R.string.ok, (a, b)->{})
|
||||
.show();
|
||||
handlePublishError(null);
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(false);
|
||||
publishButton.setEnabled(false);
|
||||
}
|
||||
|
||||
if (replyTo == null) updateRecentLanguages();
|
||||
|
@ -1315,7 +1291,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(true);
|
||||
publishButton.setEnabled(true);
|
||||
if(error instanceof MastodonErrorResponse me){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.post_failed)
|
||||
|
@ -1328,25 +1304,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
}
|
||||
|
||||
private void openPreview(Status result){
|
||||
result.preview=true;
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
(GlobalUserPreferences.relocatePublishButton ? publishButtonRelocated : publishButton).setEnabled(true);
|
||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
if(replyTo!=null){
|
||||
args.putParcelable("inReplyTo", Parcels.wrap(replyTo));
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo.account));
|
||||
}
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}
|
||||
|
||||
private void updateRecentLanguages() {
|
||||
if (postLang == null || postLang.language == null) return;
|
||||
String language = postLang.language.getLanguage();
|
||||
|
@ -1422,20 +1379,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
|
||||
private void confirmDiscardDraftAndFinish(){
|
||||
boolean attachmentsPending=mediaViewController.areAnyAttachmentsNotDone();
|
||||
if(attachmentsPending) new M3AlertDialogBuilder(getActivity())
|
||||
boolean attachmentsPending = mediaViewController.areAnyAttachmentsNotDone();
|
||||
if (attachmentsPending) new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_unfinished_attachments)
|
||||
.setMessage(R.string.sk_unfinished_attachments_message)
|
||||
.setPositiveButton(R.string.ok, (d, w)->{})
|
||||
.setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
|
||||
.setPositiveButton(R.string.edit, (d, w) -> {})
|
||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
||||
.show();
|
||||
else new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(editingStatus!=null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
||||
.setPositiveButton(R.string.save, (d, w)->{
|
||||
updateScheduledAt(scheduledAt==null ? getDraftInstant() : scheduledAt);
|
||||
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
||||
.setPositiveButton(R.string.save, (d, w) -> {
|
||||
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
|
||||
publish();
|
||||
})
|
||||
.setNegativeButton(R.string.discard, (d, w)->Nav.finish(this))
|
||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
||||
.show();
|
||||
}
|
||||
|
||||
|
@ -1453,8 +1410,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
boolean usePhotoPicker=photoPicker && UiUtils.isPhotoPickerAvailable();
|
||||
if(usePhotoPicker){
|
||||
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
if(mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount()>1)
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount());
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount());
|
||||
}else{
|
||||
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
|
@ -1511,7 +1467,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
private void openCamera() throws IOException {
|
||||
if (getContext().checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
File photoFile = File.createTempFile("img", ".jpg");
|
||||
photoUri = UiUtils.getFileProviderUri(getContext(), photoFile);
|
||||
photoUri = FileProvider.getUriForFile(getContext(), getContext().getPackageName() + ".fileprovider", photoFile);
|
||||
|
||||
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
|
||||
|
@ -1562,7 +1518,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
|
||||
public void updateSensitive() {
|
||||
sensitiveBtn.setVisibility(View.GONE);
|
||||
if (!mediaViewController.isEmpty()) sensitiveBtn.setVisibility(View.VISIBLE);
|
||||
if (!mediaViewController.isEmpty() && !hasSpoiler) sensitiveBtn.setVisibility(View.VISIBLE);
|
||||
if (mediaViewController.isEmpty()) sensitive = false;
|
||||
}
|
||||
|
||||
|
@ -1573,8 +1529,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
.withMinute(0);
|
||||
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
|
||||
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
|
||||
LocalDateTime at=LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute);
|
||||
updateScheduledAt(at.toInstant(ZoneId.systemDefault().getRules().getOffset(at)));
|
||||
updateScheduledAt(LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute)
|
||||
.toInstant(OffsetDateTime.now().getOffset()));
|
||||
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
|
||||
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
|
||||
}
|
||||
|
@ -1633,15 +1589,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
} else {
|
||||
draftsBtn.setImageDrawable(getContext().getDrawable(GlobalUserPreferences.relocatePublishButton ? R.drawable.ic_fluent_clock_24_regular : R.drawable.ic_fluent_clock_20_regular));
|
||||
if(GlobalUserPreferences.relocatePublishButton){
|
||||
publishButtonRelocated.setImageResource(R.drawable.ic_fluent_send_24_regular);
|
||||
publishButtonRelocated.setImageResource(R.drawable.ic_fluent_send_24_selector);
|
||||
}
|
||||
resetPublishButtonText();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHeaders() {
|
||||
UiUtils.setExtraTextInfo(getContext(), selfExtraText, false, false, localOnly, null);
|
||||
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account);
|
||||
UiUtils.setExtraTextInfo(getContext(), selfExtraText, null, false, false, localOnly || statusVisibility==StatusPrivacy.LOCAL, null);
|
||||
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, pronouns, true, false, replyTo.localOnly || replyTo.visibility==StatusPrivacy.LOCAL, replyTo.account);
|
||||
}
|
||||
|
||||
private void buildVisibilityPopup(View v){
|
||||
|
@ -1669,7 +1625,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()) m.setGroupDividerEnabled(true);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P) m.setGroupDividerEnabled(true);
|
||||
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item){
|
||||
|
@ -1713,7 +1669,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
}
|
||||
|
||||
contentTypePopup.setOnMenuItemClickListener(i->{
|
||||
uuid=null;
|
||||
int index=i.getItemId();
|
||||
contentType=ContentType.values()[index];
|
||||
btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal());
|
||||
|
@ -1818,26 +1773,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
||||
}
|
||||
|
||||
private String sanitizeMediaDescription(String description){
|
||||
if(description == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
// The Gboard android keyboard attaches this text whenever the user
|
||||
// pastes something from the keyboard's suggestion bar.
|
||||
// Due to different end user locales, the exact text may vary, but at
|
||||
// least in version 13.4.08, all of the translations contained the
|
||||
// string "Gboard".
|
||||
if (description.contains("Gboard")){
|
||||
return null;
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
||||
description = sanitizeMediaDescription(description);
|
||||
return mediaViewController.addMediaAttachment(uri, description);
|
||||
}
|
||||
|
||||
|
@ -1880,8 +1817,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
Editable e=mainEditText.getText();
|
||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||
if(start==-1 || end==-1)
|
||||
return;
|
||||
e.replace(start, end, text+" ");
|
||||
finishAutocomplete();
|
||||
InputConnection conn=mainEditText.getCurrentInputConnection();
|
||||
|
@ -1950,35 +1885,4 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
languageButton.setText(opt.language.getLanguageName());
|
||||
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, opt.language.getDefaultName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateEnterTransition(View prev, View container){
|
||||
AnimatorSet anim=new AnimatorSet();
|
||||
if(getArguments().getBoolean("fromThreadFragment")){
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
|
||||
);
|
||||
}else{
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
|
||||
);
|
||||
}
|
||||
anim.setDuration(300);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
return anim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateExitTransition(View prev, View container){
|
||||
AnimatorSet anim=new AnimatorSet();
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0)
|
||||
);
|
||||
anim.setDuration(200);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
return anim;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@ import android.graphics.Rect;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -23,12 +26,12 @@ import android.widget.ImageView;
|
|||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.utils.ColorPalette;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
|
@ -51,17 +54,16 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
attachmentID=getArguments().getString("attachment");
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
accountID=getArguments().getString("account");
|
||||
attachmentID=getArguments().getString("attachment");
|
||||
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
|
||||
ColorPalette.palettes.get(AccountSessionManager.get(accountID).getLocalPreferences().getCurrentColor())
|
||||
.apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
|
||||
ColorPalette.palettes.get(GlobalUserPreferences.color).apply(themeWrapper, GlobalUserPreferences.ThemePreference.DARK);
|
||||
setTitle(R.string.add_alt_text);
|
||||
}
|
||||
|
||||
|
@ -132,9 +134,20 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.help){
|
||||
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
|
||||
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
|
||||
for(BulletSpan span:spans){
|
||||
BulletSpan betterSpan;
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
|
||||
else
|
||||
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
|
||||
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
|
||||
msg.removeSpan(span);
|
||||
}
|
||||
new M3AlertDialogBuilder(themeWrapper)
|
||||
.setTitle(R.string.what_is_alt_text)
|
||||
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
|
||||
.setMessage(msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
@ -171,7 +184,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
|||
fakeAttachment.meta.width=width;
|
||||
fakeAttachment.meta.height=height;
|
||||
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
|
||||
@Override
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
image.setAlpha(visible ? 1f : 0f);
|
||||
|
|
|
@ -1,330 +0,0 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
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.view.ViewStub;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.ui.views.CurlyArrowEmptyView;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.OnBackPressedListener;
|
||||
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
|
||||
private FollowList followList;
|
||||
private Button nextButton;
|
||||
private View buttonBar;
|
||||
private FragmentRootLinearLayout rootView;
|
||||
private FrameLayout searchFragmentContainer;
|
||||
private FrameLayout fragmentContentWrap;
|
||||
private AddNewListMembersFragment searchFragment;
|
||||
private WindowInsets lastInsets;
|
||||
private boolean dismissingSearchFragment;
|
||||
private HashSet<String> accountIDsInList=new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.manage_list_members);
|
||||
setSubtitle(getString(R.string.step_x_of_y, 2, 2));
|
||||
setLayout(R.layout.fragment_login);
|
||||
setEmptyText(R.string.list_no_members);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
if(savedInstanceState!=null || getArguments().getBoolean("needLoadMembers", false)){
|
||||
loadData();
|
||||
}else{
|
||||
onDataLoaded(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetListAccounts(followList.id, null, 0)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
for(Account acc:result)
|
||||
accountIDsInList.add(acc.id);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=super.onCreateView(inflater, container, savedInstanceState);
|
||||
FrameLayout wrapper=new FrameLayout(getActivity());
|
||||
wrapper.addView(view);
|
||||
rootView=(FragmentRootLinearLayout) view;
|
||||
fragmentContentWrap=wrapper;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
nextButton=view.findViewById(R.id.btn_next);
|
||||
nextButton.setOnClickListener(this::onNextClick);
|
||||
nextButton.setText(R.string.done);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
lastInsets=insets;
|
||||
if(searchFragment!=null)
|
||||
searchFragment.onApplyWindowInsets(insets);
|
||||
insets=UiUtils.applyBottomInsetToFixedView(buttonBar, insets);
|
||||
rootView.dispatchApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
return List.of(getToolbar(), buttonBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
MenuItem item=menu.add(R.string.add_list_member);
|
||||
item.setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(searchFragmentContainer!=null)
|
||||
return true;
|
||||
|
||||
searchFragmentContainer=new FrameLayout(getActivity());
|
||||
searchFragmentContainer.setId(R.id.search_fragment);
|
||||
fragmentContentWrap.addView(searchFragmentContainer);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
args.putBoolean("_can_go_back", true);
|
||||
searchFragment=new AddNewListMembersFragment(this);
|
||||
searchFragment.setArguments(args);
|
||||
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
if(lastInsets!=null)
|
||||
searchFragment.onApplyWindowInsets(lastInsets);
|
||||
searchFragmentContainer.setTranslationX(V.dp(100));
|
||||
searchFragmentContainer.setAlpha(0f);
|
||||
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
rootView.setVisibility(View.GONE);
|
||||
}).start();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeEmptyView(View contentView){
|
||||
ViewStub emptyStub=contentView.findViewById(R.id.empty);
|
||||
emptyStub.setLayoutResource(R.layout.empty_with_arrow);
|
||||
super.initializeEmptyView(contentView);
|
||||
TextView emptySecondary=contentView.findViewById(R.id.empty_text_secondary);
|
||||
emptySecondary.setText(R.string.list_find_users);
|
||||
CurlyArrowEmptyView arrowView=(CurlyArrowEmptyView) emptyView;
|
||||
arrowView.setGravityAndOffsets(Gravity.TOP | Gravity.END, 24, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStatusBarColor(int color){
|
||||
rootView.setStatusBarColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setNavigationBarColor(int color){
|
||||
rootView.setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
private void dismissSearchFragment(){
|
||||
if(searchFragment==null || dismissingSearchFragment)
|
||||
return;
|
||||
dismissingSearchFragment=true;
|
||||
rootView.setVisibility(View.VISIBLE);
|
||||
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
fragmentContentWrap.removeView(searchFragmentContainer);
|
||||
searchFragmentContainer=null;
|
||||
searchFragment=null;
|
||||
dismissingSearchFragment=false;
|
||||
}).start();
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
private void onNextClick(View v){
|
||||
E.post(new FinishListCreationFragmentEvent(accountID, followList.id));
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(searchFragment!=null){
|
||||
dismissSearchFragment();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountInList(AccountViewModel account){
|
||||
return accountIDsInList.contains(account.account.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAccountToList(AccountViewModel account, Runnable onDone){
|
||||
new AddAccountsToList(followList.id, Set.of(account.account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
accountIDsInList.add(account.account.id);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
int i=0;
|
||||
for(AccountViewModel acc:data){
|
||||
if(acc.account.id.equals(account.account.id)){
|
||||
list.getAdapter().notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
int pos=data.size();
|
||||
data.add(account);
|
||||
list.getAdapter().notifyItemInserted(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
|
||||
new RemoveAccountsFromList(followList.id, Set.of(account.account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
accountIDsInList.remove(account.account.id);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
int i=0;
|
||||
for(AccountViewModel acc:data){
|
||||
if(acc.account.id.equals(account.account.id)){
|
||||
list.getAdapter().notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||
holder.setOnLongClickListener(vh->false);
|
||||
Button button=holder.getButton();
|
||||
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinWidth(0);
|
||||
button.setOnClickListener(v->{
|
||||
holder.setActionProgressVisible(true);
|
||||
holder.itemView.setHasTransientState(true);
|
||||
Runnable onDone=()->{
|
||||
holder.setActionProgressVisible(false);
|
||||
holder.itemView.setHasTransientState(false);
|
||||
};
|
||||
AccountViewModel account=holder.getItem();
|
||||
if(isAccountInList(account)){
|
||||
removeAccountAccountFromList(account, onDone);
|
||||
}else{
|
||||
addAccountToList(account, onDone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
Button button=holder.getButton();
|
||||
int textRes, styleRes;
|
||||
if(isAccountInList(holder.getItem())){
|
||||
textRes=R.string.remove;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||
}else{
|
||||
textRes=R.string.add;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}
|
||||
button.setText(textRes);
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base){
|
||||
// TODO this
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||
import org.joinmastodon.android.events.ListCreatedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class CreateListFragment extends BaseEditListFragment{
|
||||
private Button nextButton;
|
||||
private View buttonBar;
|
||||
private FollowList followList;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.create_list);
|
||||
setSubtitle(getString(R.string.step_x_of_y, 1, 2));
|
||||
setLayout(R.layout.fragment_login);
|
||||
if(savedInstanceState!=null)
|
||||
followList=Parcels.unwrap(savedInstanceState.getParcelable("list"));
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNavigationIconDrawableResource(){
|
||||
return R.drawable.ic_baseline_arrow_drop_down_18;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsCustomNavigationIcon(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
nextButton=view.findViewById(R.id.btn_next);
|
||||
nextButton.setOnClickListener(this::onNextClick);
|
||||
nextButton.setText(R.string.create);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
return List.of(getToolbar(), buttonBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelable("list", Parcels.wrap(followList));
|
||||
}
|
||||
|
||||
private void onNextClick(View v){
|
||||
String title=titleEdit.getText().toString().trim();
|
||||
if(TextUtils.isEmpty(title)){
|
||||
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||
return;
|
||||
}
|
||||
if(followList==null){
|
||||
new CreateList(title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
followList=result;
|
||||
proceed(false);
|
||||
E.post(new ListCreatedEvent(accountID, result));
|
||||
AccountSessionManager.get(accountID).getCacheController().addList(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}else if(!title.equals(followList.title) || getSelectedRepliesPolicy()!=followList.repliesPolicy || exclusiveItem.checked!=followList.exclusive){
|
||||
new UpdateList(followList.id, title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
followList=result;
|
||||
proceed(true);
|
||||
E.post(new ListUpdatedEvent(accountID, result));
|
||||
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}else{
|
||||
proceed(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void proceed(boolean needLoadMembers){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
args.putBoolean("needLoadMembers", needLoadMembers);
|
||||
Nav.go(getActivity(), CreateListAddMembersFragment.class, args);
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFinishListCreationFragment(FinishListCreationFragmentEvent ev){
|
||||
if(ev.accountID.equals(accountID) && followList!=null && ev.listID.equals(followList.id)){
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,14 +7,16 @@ import android.view.MenuInflater;
|
|||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
|
@ -44,14 +46,14 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
|
|||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
if (getActivity() == null) return;
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
result.stream().forEach(status -> {
|
||||
status.account.acct += "@"+domain;
|
||||
status.mentions.forEach(mention -> mention.id = null);
|
||||
|
@ -80,15 +82,12 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
|
|||
|
||||
@Override
|
||||
protected FilterContext getFilterContext() {
|
||||
return FilterContext.PUBLIC;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return new Uri.Builder()
|
||||
.scheme("https")
|
||||
.authority(domain)
|
||||
.build();
|
||||
return Uri.parse(domain);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
|
||||
public interface DomainDisplay {
|
||||
|
||||
default String getDomain(){
|
||||
AccountSession session = AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if (session != null)
|
||||
return session.domain;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class EditListFragment extends BaseEditListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.edit_list);
|
||||
loadMembers();
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
menu.add(R.string.delete_list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.delete_list)
|
||||
.setMessage(getString(R.string.delete_list_confirm, followList.title))
|
||||
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
String newTitle=titleEdit.getText().toString();
|
||||
FollowList.RepliesPolicy newRepliesPolicy=getSelectedRepliesPolicy();
|
||||
boolean newExclusive=exclusiveItem.checked;
|
||||
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
|
||||
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||
E.post(new ListUpdatedEvent(accountID, result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
// TODO handle errors somehow
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,14 +41,17 @@ import org.joinmastodon.android.api.requests.lists.GetLists;
|
|||
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CustomLocalTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -64,86 +67,86 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
|||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
|
||||
private final List<FollowList> followLists =new ArrayList<>();
|
||||
private final List<Hashtag> hashtags=new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
|
||||
private final List<ListTimeline> listTimelines = new ArrayList<>();
|
||||
private final List<Hashtag> hashtags = new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
|
||||
|
||||
public EditTimelinesFragment(){
|
||||
super(10);
|
||||
ItemTouchHelper.SimpleCallback itemTouchCallback=new ItemTouchHelperCallback();
|
||||
itemTouchHelper=new ItemTouchHelper(itemTouchCallback);
|
||||
}
|
||||
public EditTimelinesFragment() {
|
||||
super(10);
|
||||
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ;
|
||||
itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.sk_timelines);
|
||||
accountID=getArguments().getString("account");
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.sk_timelines);
|
||||
accountID = getArguments().getString("account");
|
||||
|
||||
new GetLists().setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
followLists.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> result) {
|
||||
listTimelines.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFollowedHashtags().setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
hashtags.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result) {
|
||||
hashtags.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
|
||||
}
|
||||
@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);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
this.optionsMenu=menu;
|
||||
updateOptionsMenu();
|
||||
}
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
this.optionsMenu = menu;
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.menu_back){
|
||||
updateOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_add_local_timelines) {
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_back) {
|
||||
updateOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.menu_add_local_timelines) {
|
||||
addNewLocalTimeline();
|
||||
return true;
|
||||
}
|
||||
|
@ -158,14 +161,14 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||
return true;
|
||||
}
|
||||
|
||||
private void addTimeline(TimelineDefinition tl){
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
private void addTimeline(TimelineDefinition tl) {
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
private void addNewLocalTimeline() {
|
||||
private void addNewLocalTimeline() {
|
||||
FrameLayout inputWrap = new FrameLayout(getContext());
|
||||
EditText input = new EditText(getContext());
|
||||
input.setHint(R.string.sk_example_domain);
|
||||
|
@ -191,323 +194,313 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
|||
timelineByMenuItem.put(item, tl);
|
||||
}
|
||||
|
||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon){
|
||||
MenuItem item=menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||
item.setIcon(icon);
|
||||
return item;
|
||||
}
|
||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) {
|
||||
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||
item.setIcon(icon);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void updateOptionsMenu(){
|
||||
if(getActivity()==null) return;
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
private void updateOptionsMenu() {
|
||||
if (getActivity() == null) return;
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
|
||||
SubMenu menu=optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
||||
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
||||
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
|
||||
SubMenu timelinesMenu=menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu=menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
SubMenu hashtagsMenu=menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
|
||||
MenuItem addLocalTimelines = menu.add(0, R.id.menu_add_local_timelines, NONE, R.string.local_timeline);
|
||||
MenuItem addLocalTimelines = menu.add(0, R.id.menu_add_local_timelines, NONE, R.string.local_timeline);
|
||||
addLocalTimelines.setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
|
||||
makeBackItem(timelinesMenu);
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
|
||||
followLists.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
|
||||
addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
||||
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size()>0);
|
||||
listsMenu.getItem().setVisible(listsMenu.size()>0);
|
||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.size()>0);
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
|
||||
listsMenu.getItem().setVisible(listsMenu.size() > 0);
|
||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0);
|
||||
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
|
||||
}
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
|
||||
}
|
||||
|
||||
private void saveTimelines(){
|
||||
updated=true;
|
||||
private void saveTimelines() {
|
||||
updated=true;
|
||||
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
|
||||
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
|
||||
prefs.timelines=data;
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
private void removeTimeline(int position){
|
||||
data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
private void removeTimeline(int position) {
|
||||
data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter(){
|
||||
return adapter=new TimelinesAdapter();
|
||||
}
|
||||
@Override
|
||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
|
||||
return adapter = new TimelinesAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(updated) UiUtils.restartApp();
|
||||
}
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (updated) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags){
|
||||
if(tags==null || tags.isEmpty()) return false;
|
||||
editText.setText(tags);
|
||||
editText.chipifyAllUnterminatedTokens();
|
||||
return true;
|
||||
}
|
||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
|
||||
if (tags == null || tags.isEmpty()) return false;
|
||||
editText.setText(String.join(",", tags));
|
||||
editText.chipifyAllUnterminatedTokens();
|
||||
return true;
|
||||
}
|
||||
|
||||
private NachoTextView prepareChipTextView(NachoTextView nacho){
|
||||
//I’ll Be Back
|
||||
nacho.setChipTerminators(
|
||||
Map.of(
|
||||
',', BEHAVIOR_CHIPIFY_ALL,
|
||||
'\n', BEHAVIOR_CHIPIFY_ALL,
|
||||
' ', BEHAVIOR_CHIPIFY_ALL,
|
||||
';', BEHAVIOR_CHIPIFY_ALL
|
||||
)
|
||||
);
|
||||
nacho.enableEditChipOnTouch(true, true);
|
||||
nacho.setOnFocusChangeListener((v, hasFocus)->nacho.chipifyAllUnterminatedTokens());
|
||||
return nacho;
|
||||
}
|
||||
private NachoTextView prepareChipTextView(NachoTextView nacho) {
|
||||
nacho.addChipTerminator(',', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.addChipTerminator('\n', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.addChipTerminator(' ', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.addChipTerminator(';', BEHAVIOR_CHIPIFY_ALL);
|
||||
nacho.enableEditChipOnTouch(true, true);
|
||||
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
|
||||
return nacho;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove){
|
||||
Context ctx=getContext();
|
||||
View view=getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove) {
|
||||
Context ctx = getContext();
|
||||
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||
|
||||
View divider=view.findViewById(R.id.divider);
|
||||
Button advancedBtn=view.findViewById(R.id.advanced);
|
||||
EditText editText=view.findViewById(R.id.input);
|
||||
if(item!=null) editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item!=null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||
View divider = view.findViewById(R.id.divider);
|
||||
Button advancedBtn = view.findViewById(R.id.advanced);
|
||||
EditText editText = view.findViewById(R.id.input);
|
||||
if (item != null) editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||
|
||||
LinearLayout tagWrap=view.findViewById(R.id.tag_wrap);
|
||||
boolean hashtagOptionsAvailable=item==null || item.getType()==TimelineDefinition.TimelineType.HASHTAG;
|
||||
advancedBtn.setVisibility(hashtagOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||
advancedBtn.setOnClickListener(l->{
|
||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
|
||||
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
|
||||
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||
advancedBtn.setOnClickListener(l -> {
|
||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
|
||||
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) view);
|
||||
});
|
||||
});
|
||||
|
||||
Switch localOnlySwitch=view.findViewById(R.id.local_only_switch);
|
||||
view.findViewById(R.id.local_only).setOnClickListener(l->localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
|
||||
view.findViewById(R.id.local_only)
|
||||
.setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||
|
||||
EditText tagMain=view.findViewById(R.id.tag_main);
|
||||
NachoTextView tagsAny=prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||
NachoTextView tagsAll=prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||
NachoTextView tagsNone=prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||
|
||||
if(item!=null && hashtagOptionsAvailable){
|
||||
tagMain.setText(item.getHashtagName());
|
||||
boolean hasAdvanced=!TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||
hasAdvanced=setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||
hasAdvanced=setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||
hasAdvanced=setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||
if(item.isHashtagLocalOnly()){
|
||||
localOnlySwitch.setChecked(true);
|
||||
hasAdvanced=true;
|
||||
}
|
||||
if(hasAdvanced){
|
||||
advancedBtn.setSelected(true);
|
||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||
EditText tagMain = view.findViewById(R.id.tag_main);
|
||||
NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||
NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||
if (item != null) {
|
||||
tagMain.setText(item.getHashtagName());
|
||||
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||
if (item.isHashtagLocalOnly()) {
|
||||
localOnlySwitch.setChecked(true);
|
||||
hasAdvanced = true;
|
||||
}
|
||||
if (hasAdvanced) {
|
||||
advancedBtn.setSelected(true);
|
||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||
tagWrap.setVisibility(View.VISIBLE);
|
||||
divider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton btn=view.findViewById(R.id.button);
|
||||
PopupMenu popup=new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon=item!=null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setTag(currentIcon.ordinal());
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l->popup.show());
|
||||
ImageButton btn = view.findViewById(R.id.button);
|
||||
PopupMenu popup = new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setTag(currentIcon.ordinal());
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l -> popup.show());
|
||||
|
||||
Menu menu=popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon=item!=null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if(!currentIcon.equals(defaultIcon)){
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for(TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()){
|
||||
if(icon.hidden || icon.ordinal()==(int) btn.getTag()) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
Menu menu = popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if (!currentIcon.equals(defaultIcon)) {
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
|
||||
if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
|
||||
popup.setOnMenuItemClickListener(menuItem->{
|
||||
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setTag(menuItem.getItemId());
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
return true;
|
||||
});
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setTag(menuItem.getItemId());
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
return true;
|
||||
});
|
||||
|
||||
AlertDialog.Builder builder=new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(item==null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which)->{
|
||||
String name=editText.getText().toString().trim();
|
||||
AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
tagsAny.chipifyAllUnterminatedTokens();
|
||||
tagsAll.chipifyAllUnterminatedTokens();
|
||||
tagsNone.chipifyAllUnterminatedTokens();
|
||||
String name = editText.getText().toString().trim();
|
||||
String mainHashtag = tagMain.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(mainHashtag)) {
|
||||
mainHashtag = name;
|
||||
name = null;
|
||||
}
|
||||
if (TextUtils.isEmpty(mainHashtag)) {
|
||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
||||
onSave.accept(null);
|
||||
return;
|
||||
}
|
||||
|
||||
String mainHashtag=tagMain.getText().toString().trim();
|
||||
if(item != null && item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||
tagsAny.chipifyAllUnterminatedTokens();
|
||||
tagsAll.chipifyAllUnterminatedTokens();
|
||||
tagsNone.chipifyAllUnterminatedTokens();
|
||||
if(TextUtils.isEmpty(mainHashtag)){
|
||||
mainHashtag=name;
|
||||
name=null;
|
||||
}
|
||||
if(TextUtils.isEmpty(mainHashtag) && (item!=null && item.getType()==TimelineDefinition.TimelineType.HASHTAG)){
|
||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
||||
onSave.accept(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name);
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
||||
tl.setIcon(icon);
|
||||
tl.setTitle(name);
|
||||
tl.setTagOptions(
|
||||
mainHashtag,
|
||||
tagsAny.getChipValues(),
|
||||
tagsAll.getChipValues(),
|
||||
tagsNone.getChipValues(),
|
||||
localOnlySwitch.isChecked()
|
||||
);
|
||||
onSave.accept(tl);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {});
|
||||
|
||||
TimelineDefinition tl=item!=null ? item : TimelineDefinition.ofHashtag(name);
|
||||
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
||||
tl.setIcon(icon);
|
||||
tl.setTitle(name);
|
||||
if(item == null || item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||
tl.setTagOptions(
|
||||
TextUtils.isEmpty(mainHashtag) ? name : mainHashtag,
|
||||
tagsAny.getChipValues(),
|
||||
tagsAll.getChipValues(),
|
||||
tagsNone.getChipValues(),
|
||||
localOnlySwitch.isChecked()
|
||||
);
|
||||
}
|
||||
onSave.accept(tl);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which)->{});
|
||||
if (onRemove != null) builder.setNeutralButton(R.string.sk_remove, (d, which) -> onRemove.run());
|
||||
|
||||
if(onRemove!=null) builder.setNeutralButton(R.string.sk_remove, (d, which)->onRemove.run());
|
||||
builder.show();
|
||||
btn.requestFocus();
|
||||
}
|
||||
|
||||
builder.show();
|
||||
btn.requestFocus();
|
||||
}
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new TimelineViewHolder();
|
||||
}
|
||||
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new TimelineViewHolder();
|
||||
}
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final ImageView dragger;
|
||||
|
||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final ImageView dragger;
|
||||
public TimelineViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
dragger=findViewById(R.id.dragger_thingy);
|
||||
}
|
||||
|
||||
public TimelineViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
dragger=findViewById(R.id.dragger_thingy);
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(TimelineDefinition item) {
|
||||
title.setText(item.getTitle(getContext()));
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
|
||||
dragger.setVisibility(View.VISIBLE);
|
||||
dragger.setOnTouchListener((View v, MotionEvent event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
itemTouchHelper.startDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(TimelineDefinition item){
|
||||
title.setText(item.getTitle(getContext()));
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
|
||||
dragger.setVisibility(View.VISIBLE);
|
||||
dragger.setOnTouchListener((View v, MotionEvent event)->{
|
||||
if(event.getAction()==MotionEvent.ACTION_DOWN){
|
||||
itemTouchHelper.startDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
private void onSave(TimelineDefinition tl) {
|
||||
saveTimelines();
|
||||
rebind();
|
||||
}
|
||||
|
||||
private void onSave(TimelineDefinition tl){
|
||||
saveTimelines();
|
||||
rebind();
|
||||
}
|
||||
private void onRemove() {
|
||||
removeTimeline(getAbsoluteAdapterPosition());
|
||||
}
|
||||
|
||||
private void onRemove(){
|
||||
removeTimeline(getAbsoluteAdapterPosition());
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onClick() {
|
||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onClick(){
|
||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||
}
|
||||
}
|
||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
|
||||
public ItemTouchHelperCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
||||
}
|
||||
|
||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback{
|
||||
public ItemTouchHelperCallback(){
|
||||
super(ItemTouchHelper.UP|ItemTouchHelper.DOWN, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
|
||||
}
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition = target.getAbsoluteAdapterPosition();
|
||||
if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) {
|
||||
return false;
|
||||
} else {
|
||||
Collections.swap(data, fromPosition, toPosition);
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
saveTimelines();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
|
||||
int fromPosition=viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition=target.getAbsoluteAdapterPosition();
|
||||
if(Math.max(fromPosition, toPosition)>=data.size() || Math.min(fromPosition, toPosition)<0){
|
||||
return false;
|
||||
}else{
|
||||
Collections.swap(data, fromPosition, toPosition);
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
saveTimelines();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
|
||||
viewHolder.itemView.animate().alpha(0.65f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
|
||||
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG && viewHolder!=null){
|
||||
viewHolder.itemView.animate().alpha(0.65f);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().alpha(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().alpha(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){
|
||||
int position=viewHolder.getAbsoluteAdapterPosition();
|
||||
removeTimeline(position);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAbsoluteAdapterPosition();
|
||||
removeTimeline(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
|||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
if (getActivity() == null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
|
|
@ -46,7 +46,7 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag
|
|||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, id, data.stream().filter(h -> Objects.equals(h.name, id)).findAny().map(h -> h.following).orElse(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue