Compare commits

..

1 Commits

Author SHA1 Message Date
LucasGGamerM
708331fca1 Adding the ability to long click or to double click the search icon to bring up the keyboard
(cherry picked from commit ee0737c9c7)
2023-01-04 21:55:19 -03:00
898 changed files with 4168 additions and 24326 deletions

View File

@@ -1,40 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Does this happen in the official app?**
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 Megalodon, feel free to still create this issue!
**Screenshots and screen recordings**
If applicable, add screenshots (and screen recordings, if possible) to help explain your problem.
**Version**
Moshidon version: [e.g. v1.1.4+fork.#]
**Crash log**
If you know your way around Android development tools, please consider attaching a crash log, if possible.

View File

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

View File

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

View File

@@ -1,48 +0,0 @@
name: Nightly builds
on:
push:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
cache: gradle
- name: Get current date
id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1
with:
fileName: 'nightly_keystore.jks'
fileDir: './mastodon/keystore/'
encodedString: ${{ secrets.KEYSTORE }}
- name: Build with Gradle
run: ./gradlew assembleNightly
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
CURRENT_DATE: ${{ steps.date.outputs.date }}
- name: Upload a Build Artifact
uses: actions/upload-artifact@v3.1.2
with:
name: moshidon-nightly.apk
path: ./mastodon/build/outputs/apk/nightly/moshidon-nightly.apk

View File

@@ -1,11 +0,0 @@
name: Validate Gradle Wrapper
on: [pull_request, push]
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1

2
.gitignore vendored
View File

@@ -9,5 +9,3 @@
.cxx .cxx
local.properties local.properties
*.jks *.jks
*.keystore
/mastodon/keystore/nightly_keystore.keystore

5
FAQ.md
View File

@@ -1,5 +0,0 @@
## F.A.Q
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.

View File

@@ -5,49 +5,27 @@
> A fork of [megalodon](https://github.com/sk22/megalodon) which is a fork of [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly wont ever be implemented, such as the federated timeline, unlisted posting, bookmarks and an image description viewer. > A fork of [megalodon](https://github.com/sk22/megalodon) which is a fork of [official Mastodon Android app](https://github.com/mastodon/mastodon-android) adding important features that are missing in the official app and possibly wont ever be implemented, such as the federated timeline, unlisted posting, bookmarks and an image description viewer.
[![Download latest release](https://img.shields.io/badge/dynamic/json?color=282C37&label=Download%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLucasGGamerM%2Fmoshidon%2Freleases%2Flatest&style=for-the-badge)](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk) [![Download latest release](https://img.shields.io/badge/dynamic/json?color=d92aad&label=download%20apk&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLucasGGamerM%2Fmoshidon%2Freleases%2Flatest&style=for-the-badge)](https://github.com/LucasGGamerM/moshidon/releases/latest/download/moshidon.apk)
[![Download nightly release](https://img.shields.io/badge/dynamic/json?color=282C37&label=Download%20Nightly%20APK&query=%24.tag_name&url=https%3A%2F%2Fapi.github.com%2Frepos%2FLucasGGamerM%2Fmoshidon%2Freleases%2Flatest&style=for-the-badge)](https://github.com/LucasGGamerM/moshidon-nightly/releases/latest/download/moshidon-nightly.apk)
[![Translation status](https://translate.codeberg.org/widgets/moshidon/-/svg-badge.svg)](https://translate.codeberg.org/engage/moshidon/)
 
[![Nightly build](https://github.com/LucasGGamerM/moshidon/actions/workflows/nightly-builds.yml/badge.svg)](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="50" alt="Get it on Google Play" src="img/google-play-badge.png"></a>
&nbsp;
<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>
## Help out the project by donating at: https://github.com/sponsors/LucasGGamerM!
--- ---
## Key features ## Key features
### **The ability to add other server's local timeline to your timelines** ### **Material you theme support on Android 12+ devices!**
It can be accessed in the "Edit timelines" menu, where you can add a new "Community" to see other server's local posts! ### **Translate button**
### **View remote profiles** **Allows you to translate posts in instances with the translate feature!**
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. **Screenshots**
### **Translate posts easily**
Allows you to easily translate posts in another language with a translate button! Your instance must support translation, otherwise it will not work.
### **Show posts filtered with a warning**
Allows you to have filtered posts collapsed with a warning! As shown in the screenshots:
Before | After
:-------------------------:|:-------------------------:
![Screenshot_20230205-100200edited](https://user-images.githubusercontent.com/71328265/216820539-20802dc5-e433-4511-b2d9-291d810e4ef2.png) | ![Screenshot_20230205-100203edited](https://user-images.githubusercontent.com/71328265/216820544-231b2966-f38f-4ec6-b555-d39c62433839.png)
![Screenshot_20221209-135457_1](https://user-images.githubusercontent.com/71328265/206753830-cdb8bc65-7732-4a6a-8bcd-bbc4ca311d19.png)
![Screenshot_20221209-135409_1](https://user-images.githubusercontent.com/71328265/206753831-7af92a48-d7a5-4780-9beb-90acef4e141b.png)
### **Color themes** ### **Color themes**
**Allows you to change theme within the app. Supports Material You, purple, pink, green, blue, red, orange, yellow and Nord!** **Allows you to change theme within the app. Supports Purple, pink, green, blue, orange and yellow!**
### **Unlisted posting** ### **Unlisted posting**
@@ -71,10 +49,6 @@ Thats one of the reasons why choosing a small, **well-moderated instance is i
This is important to **ensure the content youre sharing is as accessible as possible** to people who cant see the images and rely on software to read back the provided content descriptions. Thankfully, its quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way! This is important to **ensure the content youre sharing is as accessible as possible** to people who cant see the images and rely on software to read back the provided content descriptions. Thankfully, its quite common for people on the Fediverse to provide such alt texts, and hopefully things stay this way!
### **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** ### **Pinning posts**
**This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in peoples profiles shows all the posts they pinned.** **This lets you can highlight important posts on your profile. A dedicated “Pinned” tab in peoples profiles shows all the posts they pinned.**
@@ -93,28 +67,18 @@ To bookmark a post, press the button between the Favorite and Share buttons on t
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. 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 makes use of [Mastodon for Android](https://github.com/mastodon/mastodon-android)s automatic update checker. Megalodon will check for new updates available on GitHub and offer to download and install them. You can also manually press “Check for updates” at the bottom of the settings page!
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 ## Release variants
### Stable variant All downloads can be found on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
All stable version downloads can be found on the [Releases](https://github.com/LucasGGamerM/moshidon/releases) page.
**`moshidon.apk`** **`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`. 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.
--- ---
@@ -122,18 +86,9 @@ Unstable variant with an integrated updater. It's for development and testing pu
### Features ### 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) * [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)) ([Pull request](https://github.com/mastodon/mastodon-android/pull/103))
* Adding a useful private profile note box * [Add “Federation” tab and change Discover tab order](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/add-federated-timeline) ([Closes issue](https://github.com/mastodon/mastodon-android/issues/8))
* 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)) * [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 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 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))
@@ -155,8 +110,6 @@ Unstable variant with an integrated updater. It's for development and testing pu
### Behavior ### 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)) * [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)) * [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)) * [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))
@@ -187,10 +140,4 @@ This project is released under the [GPL-3 License](./LICENSE).
## Links ## Links
[F.A.Q](FAQ.md)
[Official matrix chatroom:](https://matrix.to/#/#moshidon:floss.social) https://matrix.to/#/#moshidon:floss.social
<a rel="me" href="https://floss.social/@moshidon">@moshidon<wbr>@floss.social</a> <a rel="me" href="https://floss.social/@moshidon">@moshidon<wbr>@floss.social</a>
---

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moshidon</title> <title>Moshidon</title>
<link rel="icon" href="mastodon/src/main/res/mipmap-mdpi/ic_launcher_round.png"> <link rel="icon" href="mastodon/src/main/res/mipmap-mdpi/ic_launcher_round.png">
<link rel="me" href="https://floss.social/@megalodon"> <link rel="me" href="https://floss.social/@mastodon">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.1.0/github-markdown.min.css">
</head> </head>
<body class="markdown-body"> <body class="markdown-body">

View File

@@ -5,7 +5,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.0.0' classpath 'com.android.tools.build:gradle:7.3.1'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }

5
crowdin.yml Normal file
View File

@@ -0,0 +1,5 @@
files:
- source: /mastodon/src/main/res/values/strings.xml
translation: /mastodon/src/main/res/values-%android_code%/strings.xml
- source: /fastlane/metadata/android/en-US/*.txt
translation: /fastlane/metadata/android/%locale%/%original_file_name%

View File

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

View File

@@ -16,7 +16,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=false android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=true
android.nonFinalResIds=false

Binary file not shown.

View File

@@ -1,7 +1,6 @@
#Thu Jan 13 11:33:43 MSK 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

276
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/usr/bin/env sh
# #
# Copyright © 2015-2021 the original authors. # Copyright 2015 the original author or authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -17,98 +17,67 @@
# #
############################################################################## ##############################################################################
# ##
# Gradle start up script for POSIX generated by Gradle. ## Gradle start up script for UN*X
# ##
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
app_path=$0 PRG="$0"
# Need this for relative symlinks.
# Need this for daisy-chained symlinks. while [ -h "$PRG" ] ; do
while ls=`ls -ld "$PRG"`
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path link=`expr "$ls" : '.*-> \(.*\)$'`
[ -h "$app_path" ] if expr "$link" : '/.*' > /dev/null; then
do PRG="$link"
ls=$( ls -ld "$app_path" ) else
link=${ls#*' -> '} PRG=`dirname "$PRG"`"/$link"
case $link in #( fi
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
# This is normally unused APP_NAME="Gradle"
# shellcheck disable=SC2034 APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD="maximum"
warn () { warn () {
echo "$*" echo "$*"
} >&2 }
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} >&2 }
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "$( uname )" in #( case "`uname`" in
CYGWIN* ) cygwin=true ;; #( CYGWIN* )
Darwin* ) darwin=true ;; #( cygwin=true
MSYS* | MINGW* ) msys=true ;; #( ;;
NONSTOP* ) nonstop=true ;; Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -118,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java JAVACMD="$JAVA_HOME/jre/sh/java"
else else
JAVACMD=$JAVA_HOME/bin/java JAVACMD="$JAVA_HOME/bin/java"
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -129,7 +98,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@@ -137,109 +106,80 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
case $MAX_FD in #( MAX_FD_LIMIT=`ulimit -H -n`
max*) if [ $? -eq 0 ] ; then
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
# shellcheck disable=SC3045 MAX_FD="$MAX_FD_LIMIT"
MAX_FD=$( ulimit -H -n ) || fi
warn "Could not query maximum file descriptor limit" ulimit -n $MAX_FD
esac if [ $? -ne 0 ] ; then
case $MAX_FD in #( warn "Could not set maximum file descriptor limit: $MAX_FD"
'' | soft) :;; #( fi
*) else
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
# shellcheck disable=SC3045 fi
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi fi
# Collect all arguments for the java command, stacking in reverse order: # For Darwin, add options to specify how the application appears in the dock
# * args from the command line if $darwin; then
# * the main class name GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
# * -classpath fi
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=`cygpath --unix "$JAVACMD"`
# Now convert the arguments - kludge to limit ourselves to /bin/sh # We build the pattern for arguments to be converted via cygpath
for arg do ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
if SEP=""
case $arg in #( for dir in $ROOTDIRSRAW ; do
-*) false ;; # don't mess with options #( ROOTDIRS="$ROOTDIRS$SEP$dir"
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath SEP="|"
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Collect all arguments for the java command, following the shell quoting and substitution rules
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

15
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd if "%ERRORLEVEL%"=="0" goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL% if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
if %EXIT_CODE% equ 0 set EXIT_CODE=1 exit /b 1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -2,78 +2,33 @@ plugins {
id 'com.android.application' id 'com.android.application'
} }
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
android { android {
compileSdk 33 compileSdk 33
defaultConfig { defaultConfig {
manifestPlaceholders = [oAuthScheme:"moshidon-android-auth"]
archivesBaseName = "moshidon" archivesBaseName = "moshidon"
applicationId "org.joinmastodon.android.moshinda" applicationId "org.joinmastodon.android.moshinda"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 100 versionCode 84
versionName "1.3.0+fork.100.moshinda" versionName "1.1.4+fork.84.moshinda"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW'] resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}
signingConfigs {
nightly{
storeFile = file("keystore/nightly_keystore.jks")
storePassword System.getenv("SIGNING_STORE_PASSWORD")
if (storePassword == null) {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
storePassword = properties.getProperty('SIGNING_STORE_PASSWORD')
}
keyAlias System.getenv("SIGNING_KEY_ALIAS")
if (keyAlias == null) {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
keyAlias = properties.getProperty('SIGNING_KEY_ALIAS')
}
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
if (keyPassword == null) {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
keyPassword = properties.getProperty('SIGNING_KEY_PASSWORD')
}
}
} }
buildTypes { buildTypes {
release { release {
minifyEnabled true // minifyEnabled true
shrinkResources true // shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug{ debug{
debuggable true debuggable true
versionNameSuffix '-debug' versionNameSuffix '-debug'
applicationIdSuffix '.debug' applicationIdSuffix '.debug'
manifestPlaceholders = [oAuthScheme:"moshidon-android-debug-auth"]
} }
githubRelease{ githubRelease{
initWith release initWith release
} }
nightly{
initWith release
if(System.getenv("CURRENT_DATE") != null){
versionNameSuffix '-nightly+@' + System.getenv("CURRENT_DATE")
} else {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
versionNameSuffix '-nightly+@' + properties.getProperty('CURRENT_DATE')
}
applicationIdSuffix '.nightly'
signingConfig signingConfigs.nightly
manifestPlaceholders = [oAuthScheme:"moshidon-android-nightly-auth"]
}
playRelease{ playRelease{
initWith release initWith release
minifyEnabled true minifyEnabled true
@@ -91,22 +46,17 @@ android {
setRoot "src/github" setRoot "src/github"
} }
debug { debug {
setRoot "src/debug" setRoot "src/github"
} }
} }
namespace 'org.joinmastodon.android' lintOptions{
lint {
abortOnError false
checkReleaseBuilds false checkReleaseBuilds false
} abortOnError false
buildFeatures {
buildConfig true
} }
} }
dependencies { dependencies {
api 'androidx.annotation:annotation:1.6.0' api 'androidx.annotation:annotation:1.3.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.9' implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'me.grishka.litex:recyclerview:1.2.1.1' implementation 'me.grishka.litex:recyclerview:1.2.1.1'
implementation 'me.grishka.litex:swiperefreshlayout:1.1.0.1' implementation 'me.grishka.litex:swiperefreshlayout:1.1.0.1'
@@ -115,12 +65,11 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0' implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0' implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.7' implementation 'me.grishka.appkit:appkit:1.2.7'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3' implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8' implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3' implementation 'de.psdev:async-otto:1.0.3'
implementation 'org.parceler:parceler-api:1.1.12' implementation 'org.parceler:parceler-api:1.1.12'
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
annotationProcessor 'org.parceler:parceler:1.1.12' annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'

View File

@@ -42,13 +42,6 @@
-keepattributes LineNumberTable -keepattributes LineNumberTable
-keepattributes *
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
#-keep class javax.** { *; }
-keep class org.joinmastodon.android.** { *; }
# Parceler library # Parceler library
-keep interface org.parceler.Parcel -keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; } -keep @org.parceler.Parcel class * { *; }

View File

@@ -1,369 +0,0 @@
package org.joinmastodon.android.updater;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInstaller;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import java.io.File;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.Keep;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@Keep
public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
private static final long CHECK_PERIOD=6*3600*1000L;
private static final String TAG="GithubSelfUpdater";
private UpdateState state=UpdateState.NO_UPDATE;
private UpdateInfo info;
private long downloadID;
private BroadcastReceiver downloadCompletionReceiver=new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent){
if(downloadID!=0 && intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)==downloadID){
MastodonApp.context.unregisterReceiver(this);
setState(UpdateState.DOWNLOADED);
}
}
};
public GithubSelfUpdaterImpl(){
SharedPreferences prefs=getPrefs();
int checkedByBuild=prefs.getInt("checkedByBuild", 0);
if(prefs.contains("version") && checkedByBuild==BuildConfig.VERSION_CODE){
info=new UpdateInfo();
info.version=prefs.getString("version", null);
info.size=prefs.getLong("apkSize", 0);
info.changelog=prefs.getString("changelog", null);
downloadID=prefs.getLong("downloadID", 0);
if(downloadID==0 || !getUpdateApkFile().exists()){
state=UpdateState.UPDATE_AVAILABLE;
}else{
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
state=dm.getUriForDownloadedFile(downloadID)==null ? UpdateState.DOWNLOADING : UpdateState.DOWNLOADED;
if(state==UpdateState.DOWNLOADING){
MastodonApp.context.registerReceiver(downloadCompletionReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
}else if(checkedByBuild!=BuildConfig.VERSION_CODE && checkedByBuild>0){
// We are in a new version, running for the first time after update. Gotta clean things up.
long id=getPrefs().getLong("downloadID", 0);
if(id!=0){
MastodonApp.context.getSystemService(DownloadManager.class).remove(id);
}
getUpdateApkFile().delete();
getPrefs().edit()
.remove("apkSize")
.remove("version")
.remove("apkURL")
.remove("checkedByBuild")
.remove("downloadID")
.remove("changelog")
.apply();
}
}
private SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("githubUpdater", Context.MODE_PRIVATE);
}
@Override
public void maybeCheckForUpdates(){
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
return;
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", CHECK_PERIOD);
if(timeSinceLastCheck>=CHECK_PERIOD){
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
}
@Override
public void checkForUpdates() {
setState(UpdateState.CHECKING);
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
}
private void actuallyCheckForUpdates(){
Request req=new Request.Builder()
.url("https://api.github.com/repos/LucasGGamerM/moshidon/releases")
.build();
Call call=MastodonAPIController.getHttpClient().newCall(req);
try(Response resp=call.execute()){
JsonArray arr=JsonParser.parseReader(resp.body().charStream()).getAsJsonArray();
for (JsonElement jsonElement : arr) {
JsonObject obj = jsonElement.getAsJsonObject();
if (obj.get("prerelease").getAsBoolean() && !GlobalUserPreferences.enablePreReleases) continue;
String tag=obj.get("tag_name").getAsString();
String changelog=obj.get("body").getAsString();
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
Matcher matcher=pattern.matcher(tag);
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
return;
}
int newMajor=Integer.parseInt(matcher.group(1)),
newMinor=Integer.parseInt(matcher.group(2)),
newRevision=Integer.parseInt(matcher.group(3)),
newForkNumber=Integer.parseInt(matcher.group(4));
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
String[] currentParts=BuildConfig.VERSION_NAME.split("[.+]");
if(!matcher.find()){
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
return;
}
int curMajor=Integer.parseInt(matcher.group(1)),
curMinor=Integer.parseInt(matcher.group(2)),
curRevision=Integer.parseInt(matcher.group(3)),
curForkNumber=Integer.parseInt(matcher.group(4));
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
if(newVersion>curVersion || newForkNumber>curForkNumber){
String version=newMajor+"."+newMinor+"."+newRevision+"+fork."+newForkNumber;
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
for(JsonElement el:obj.getAsJsonArray("assets")){
JsonObject asset=el.getAsJsonObject();
if("moshidon.apk".equals(asset.get("name").getAsString()) && "application/vnd.android.package-archive".equals(asset.get("content_type").getAsString()) && "uploaded".equals(asset.get("state").getAsString())){
long size=asset.get("size").getAsLong();
String url=asset.get("browser_download_url").getAsString();
UpdateInfo info=new UpdateInfo();
info.size=size;
info.version=version;
info.changelog=changelog;
this.info=info;
getPrefs().edit()
.putLong("apkSize", size)
.putString("version", version)
.putString("apkURL", url)
.putString("changelog", changelog)
.putInt("checkedByBuild", BuildConfig.VERSION_CODE)
.remove("downloadID")
.apply();
break;
}
}
}
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
break;
}
}catch(Exception x){
Log.w(TAG, "actuallyCheckForUpdates", x);
}finally{
setState(info==null ? UpdateState.NO_UPDATE : UpdateState.UPDATE_AVAILABLE);
}
}
private void setState(UpdateState state){
this.state=state;
E.post(new SelfUpdateStateChangedEvent(state));
}
@Override
public UpdateState getState(){
return state;
}
@Override
public UpdateInfo getUpdateInfo(){
return info;
}
public File getUpdateApkFile(){
return new File(MastodonApp.context.getExternalCacheDir(), "update.apk");
}
@Override
public void downloadUpdate(){
if(state==UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
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()))
);
getPrefs().edit().putLong("downloadID", downloadID).apply();
setState(UpdateState.DOWNLOADING);
}
@Override
public void installUpdate(Activity activity){
if(state!=UpdateState.DOWNLOADED)
throw new IllegalStateException();
Uri uri;
Intent intent=new Intent(Intent.ACTION_INSTALL_PACKAGE);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
uri=new Uri.Builder().scheme("content").authority(activity.getPackageName()+".self_update_provider").path("update.apk").build();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}else{
uri=Uri.fromFile(getUpdateApkFile());
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
activity.startActivity(intent);
// TODO figure out how to restart the app when updating via this new API
/*
PackageInstaller installer=activity.getPackageManager().getPackageInstaller();
try{
final int sid=installer.createSession(new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL));
installer.registerSessionCallback(new PackageInstaller.SessionCallback(){
@Override
public void onCreated(int i){
}
@Override
public void onBadgingChanged(int i){
}
@Override
public void onActiveChanged(int i, boolean b){
}
@Override
public void onProgressChanged(int id, float progress){
}
@Override
public void onFinished(int id, boolean success){
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
}
});
activity.getPackageManager().setComponentEnabledSetting(new ComponentName(activity, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
PackageInstaller.Session session=installer.openSession(sid);
try(OutputStream out=session.openWrite("mastodon.apk", 0, info.size); InputStream in=new FileInputStream(getUpdateApkFile())){
byte[] buffer=new byte[16384];
int read;
while((read=in.read(buffer))>0){
out.write(buffer, 0, read);
}
}
// PendingIntent intent=PendingIntent.getBroadcast(activity, 1, new Intent(activity, InstallerStatusReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE);
PendingIntent intent=PendingIntent.getActivity(activity, 1, new Intent(activity, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
session.commit(intent.getIntentSender());
}catch(IOException x){
Log.w(TAG, "installUpdate", x);
Toast.makeText(activity, x.getMessage(), Toast.LENGTH_SHORT).show();
}
*/
}
@Override
public float getDownloadProgress(){
if(state!=UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
try(Cursor cursor=dm.query(new DownloadManager.Query().setFilterById(downloadID))){
if(cursor.moveToFirst()){
long loaded=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
long total=cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
// Log.d(TAG, "getDownloadProgress: "+loaded+" of "+total);
return total>0 ? (float)loaded/total : 0f;
}
}
return 0;
}
@Override
public void cancelDownload(){
if(state!=UpdateState.DOWNLOADING)
throw new IllegalStateException();
DownloadManager dm=MastodonApp.context.getSystemService(DownloadManager.class);
dm.remove(downloadID);
downloadID=0;
getPrefs().edit().remove("downloadID").apply();
setState(UpdateState.UPDATE_AVAILABLE);
}
@Override
public void handleIntentFromInstaller(Intent intent, Activity activity){
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
activity.startActivity(confirmIntent);
}else if(status!=PackageInstaller.STATUS_SUCCESS){
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(activity, activity.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
}
}
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent){
int status=intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
if(status==PackageInstaller.STATUS_PENDING_USER_ACTION){
Intent confirmIntent=intent.getParcelableExtra(Intent.EXTRA_INTENT);
context.startActivity(confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}else if(status!=PackageInstaller.STATUS_SUCCESS){
String msg=intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(context, context.getString(R.string.error)+":\n"+msg, Toast.LENGTH_LONG).show();
}
}
}
public static class AfterUpdateRestartReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent){
if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())){
context.getPackageManager().setComponentEnabledSetting(new ComponentName(context, AfterUpdateRestartReceiver.class), PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
Toast.makeText(context, R.string.update_installed, Toast.LENGTH_SHORT).show();
Intent restartIntent=new Intent(context, MainActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setPackage(context.getPackageName());
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.P){
context.startActivity(restartIntent);
}else{
// Bypass activity starting restrictions by starting it from a notification
NotificationManager nm=context.getSystemService(NotificationManager.class);
NotificationChannel chan=new NotificationChannel("selfUpdateRestart", context.getString(R.string.update_installed), NotificationManager.IMPORTANCE_HIGH);
nm.createNotificationChannel(chan);
Notification n=new Notification.Builder(context, "selfUpdateRestart")
.setContentTitle(context.getString(R.string.update_installed))
.setContentIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
.setFullScreenIntent(PendingIntent.getActivity(context, 1, restartIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE), true)
.setSmallIcon(R.drawable.ic_ntf_logo)
.build();
nm.notify(1, n);
}
}
}
}*/
}

View File

@@ -1,62 +0,0 @@
package org.joinmastodon.android.updater;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.FileNotFoundException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SelfUpdateContentProvider extends ContentProvider{
@Override
public boolean onCreate(){
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri){
if(isCorrectUri(uri))
return "application/vnd.android.package-archive";
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
return 0;
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
if(isCorrectUri(uri)){
return ParcelFileDescriptor.open(((GithubSelfUpdaterImpl)GithubSelfUpdater.getInstance()).getUpdateApkFile(), ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException();
}
private boolean isCorrectUri(Uri uri){
return "/update.apk".equals(uri.getPath());
}
}

View File

@@ -1,20 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FF000000"
android:pathData="M54,90L54,90c-19.9,0 -36,-16.1 -36,-36v0c0,-19.9 16.1,-36 36,-36h0c19.9,0 36,16.1 36,36v0C90,73.9 73.9,90 54,90z"
android:strokeAlpha="0"
android:fillAlpha="0"/>
<path
android:pathData="M52.5,41.6c-2.4,0 -4.3,0.9 -5.5,2.8l-1.2,2l-1.2,-2c-1.2,-1.9 -3.1,-2.8 -5.5,-2.8c-2.1,0 -3.8,0.8 -5.1,2.2c-1.2,1.4 -1.9,3.4 -1.9,5.9v12h4.7V50c0,-2.4 1.1,-3.7 3.1,-3.7c2.3,0 3.4,1.4 3.4,4.4v6.4h4.7v-6.4c0,-2.9 1.1,-4.4 3.4,-4.4c2.1,0 3.1,1.2 3.1,3.7v11.7h4.7v-12c0,-2.4 -0.6,-4.4 -1.9,-5.9C56.2,42.3 54.6,41.6 52.5,41.6z"
android:fillColor="#33D17A"/>
<path
android:pathData="M65.9,58.1h0.8c0,0 0,0 -0.1,0c-0.6,-0.3 -1.1,-0.8 -1.4,-1.4c-0.3,-0.6 -0.5,-1.4 -0.5,-2.1c0,-0.8 0.2,-1.5 0.5,-2.1c0.4,-0.6 0.8,-1.1 1.4,-1.4c0.6,-0.3 1.2,-0.5 1.9,-0.5c0.7,0 1.3,0.2 1.9,0.5s1.1,0.8 1.4,1.4s0.5,1.3 0.5,2.1c0,0.2 0,0.4 0,0.6l0.7,0.7c0.4,0 0.8,0 1.1,0l1.5,-1.5l0.2,-0.2c-0.1,-1.2 -0.4,-2.3 -0.9,-3.4c-0.6,-1.1 -1.4,-2 -2.6,-2.6c-1.1,-0.6 -2.4,-1 -3.7,-1c-1.4,0 -2.7,0.3 -3.8,1c-1.1,0.6 -2,1.5 -2.6,2.6c-0.6,1.1 -0.9,2.4 -0.9,3.7s0.3,2.7 0.9,3.7c0.6,1.1 1.5,2 2.6,2.6c0.4,0.2 0.8,0.4 1.1,0.5v-1.8V58.1z"
android:fillColor="#33D17A"/>
<path
android:pathData="M76,58.3l1.2,-1.2L76.2,56l-1.7,1.7c-0.4,-0.1 -0.7,-0.2 -1.1,-0.2s-0.8,0.1 -1.1,0.2L70.7,56l-1,1.1l1.2,1.2c-0.5,0.4 -1.1,0.9 -1.4,1.5h-2.1v1.5H69c0,0.2 -0.1,0.5 -0.1,0.8v0.8h-1.5v1.5h1.5v0.8c0,0.2 0,0.5 0.1,0.8h-1.6v1.5h2.1c0.8,1.4 2.3,2.3 4,2.3s3.1,-0.9 4,-2.3h2.1v-1.5H78c0,-0.2 0.1,-0.5 0.1,-0.8v-0.8h1.5v-1.5h-1.5v-0.8c0,-0.2 0,-0.5 -0.1,-0.8h1.6v-1.5h-2.1C77.1,59.2 76.6,58.8 76,58.3zM75,65.9H72v-1.5H75V65.9zM75,62.9H72v-1.5H75V62.9z"
android:fillColor="#33D17A"/>
</vector>

View File

@@ -1,20 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FF000000"
android:pathData="M54,90L54,90c-19.9,0 -36,-16.1 -36,-36v0c0,-19.9 16.1,-36 36,-36h0c19.9,0 36,16.1 36,36v0C90,73.9 73.9,90 54,90z"
android:strokeAlpha="0"
android:fillAlpha="0"/>
<path
android:pathData="M52.5,41.6c-2.4,0 -4.3,0.9 -5.5,2.8l-1.2,2l-1.2,-2c-1.2,-1.9 -3.1,-2.8 -5.5,-2.8c-2.1,0 -3.8,0.8 -5.1,2.2c-1.2,1.4 -1.9,3.4 -1.9,5.9v12h4.7V50c0,-2.4 1.1,-3.7 3.1,-3.7c2.3,0 3.4,1.4 3.4,4.4v6.4h4.7v-6.4c0,-2.9 1.1,-4.4 3.4,-4.4c2.1,0 3.1,1.2 3.1,3.7v11.7h4.7v-12c0,-2.4 -0.6,-4.4 -1.9,-5.9C56.2,42.3 54.6,41.6 52.5,41.6z"
android:fillColor="#33D17A"/>
<path
android:pathData="M65.9,58.1h0.8c0,0 0,0 -0.1,0c-0.6,-0.3 -1.1,-0.8 -1.4,-1.4c-0.3,-0.6 -0.5,-1.4 -0.5,-2.1c0,-0.8 0.2,-1.5 0.5,-2.1c0.4,-0.6 0.8,-1.1 1.4,-1.4c0.6,-0.3 1.2,-0.5 1.9,-0.5c0.7,0 1.3,0.2 1.9,0.5s1.1,0.8 1.4,1.4s0.5,1.3 0.5,2.1c0,0.2 0,0.4 0,0.6l0.7,0.7c0.4,0 0.8,0 1.1,0l1.5,-1.5l0.2,-0.2c-0.1,-1.2 -0.4,-2.3 -0.9,-3.4c-0.6,-1.1 -1.4,-2 -2.6,-2.6c-1.1,-0.6 -2.4,-1 -3.7,-1c-1.4,0 -2.7,0.3 -3.8,1c-1.1,0.6 -2,1.5 -2.6,2.6c-0.6,1.1 -0.9,2.4 -0.9,3.7s0.3,2.7 0.9,3.7c0.6,1.1 1.5,2 2.6,2.6c0.4,0.2 0.8,0.4 1.1,0.5v-1.8V58.1z"
android:fillColor="#33D17A"/>
<path
android:pathData="M76,58.3l1.2,-1.2L76.2,56l-1.7,1.7c-0.4,-0.1 -0.7,-0.2 -1.1,-0.2s-0.8,0.1 -1.1,0.2L70.7,56l-1,1.1l1.2,1.2c-0.5,0.4 -1.1,0.9 -1.4,1.5h-2.1v1.5H69c0,0.2 -0.1,0.5 -0.1,0.8v0.8h-1.5v1.5h1.5v0.8c0,0.2 0,0.5 0.1,0.8h-1.6v1.5h2.1c0.8,1.4 2.3,2.3 4,2.3s3.1,-0.9 4,-2.3h2.1v-1.5H78c0,-0.2 0.1,-0.5 0.1,-0.8v-0.8h1.5v-1.5h-1.5v-0.8c0,-0.2 0,-0.5 -0.1,-0.8h1.6v-1.5h-2.1C77.1,59.2 76.6,58.8 76,58.3zM75,65.9H72v-1.5H75V65.9zM75,62.9H72v-1.5H75V62.9z"
android:fillColor="#33D17A"/>
</vector>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground_debug"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground_monochrome_debug"/>
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground_debug"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground_monochrome_debug"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View File

@@ -1,14 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.joinmastodon.android"> package="org.joinmastodon.android">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application>
<application
tools:replace="android:label"
android:label="@string/mo_app_name_debug">
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$InstallerStatusReceiver" android:exported="false"/>--> <!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$InstallerStatusReceiver" android:exported="false"/>-->
<!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$AfterUpdateRestartReceiver" android:exported="true" android:enabled="false">--> <!-- <receiver android:name=".updater.GithubSelfUpdaterImpl$AfterUpdateRestartReceiver" android:exported="true" android:enabled="false">-->
<!-- <intent-filter>--> <!-- <intent-filter>-->

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -14,14 +14,12 @@ import android.os.Build;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
@@ -115,17 +113,13 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
private void actuallyCheckForUpdates(){ private void actuallyCheckForUpdates(){
Request req=new Request.Builder() Request req=new Request.Builder()
.url("https://api.github.com/repos/LucasGGamerM/moshidon/releases") .url("https://api.github.com/repos/LucasGGamerM/moshidon/releases/latest")
.build(); .build();
Call call=MastodonAPIController.getHttpClient().newCall(req); Call call=MastodonAPIController.getHttpClient().newCall(req);
try(Response resp=call.execute()){ try(Response resp=call.execute()){
JsonArray arr=JsonParser.parseReader(resp.body().charStream()).getAsJsonArray(); JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
for (JsonElement jsonElement : arr) {
JsonObject obj = jsonElement.getAsJsonObject();
if (obj.get("prerelease").getAsBoolean() && !GlobalUserPreferences.enablePreReleases) continue;
String tag=obj.get("tag_name").getAsString();
String changelog=obj.get("body").getAsString(); String changelog=obj.get("body").getAsString();
String tag=obj.get("tag_name").getAsString();
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)"); Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)\\+fork\\.(\\d+)");
Matcher matcher=pattern.matcher(tag); Matcher matcher=pattern.matcher(tag);
if(!matcher.find()){ if(!matcher.find()){
@@ -167,8 +161,8 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
.putLong("apkSize", size) .putLong("apkSize", size)
.putString("version", version) .putString("version", version)
.putString("apkURL", url) .putString("apkURL", url)
.putString("changelog", changelog)
.putInt("checkedByBuild", BuildConfig.VERSION_CODE) .putInt("checkedByBuild", BuildConfig.VERSION_CODE)
.putString("changelog", changelog)
.remove("downloadID") .remove("downloadID")
.apply(); .apply();
@@ -177,8 +171,6 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
} }
} }
getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply(); getPrefs().edit().putLong("lastCheck", System.currentTimeMillis()).apply();
break;
}
}catch(Exception x){ }catch(Exception x){
Log.w(TAG, "actuallyCheckForUpdates", x); Log.w(TAG, "actuallyCheckForUpdates", x);
}finally{ }finally{

View File

@@ -12,26 +12,15 @@
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/> <permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<intent>
<action android:name="android.intent.action.TRANSLATE" />
</intent>
</queries>
<application <application
android:name=".MastodonApp" android:name=".MastodonApp"
android:allowBackup="true" android:allowBackup="true"
android:label="@string/mo_app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Mastodon.AutoLightDark" android:theme="@style/Theme.Mastodon.AutoLightDark"
android:windowSoftInputMode="adjustPan"
android:largeHeap="true"> android:largeHeap="true">
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask"> <activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">
@@ -45,11 +34,10 @@
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="${oAuthScheme}" android:host="callback"/> <data android:scheme="moshidon-android-auth" android:host="callback"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" <activity android:name=".ExternalShareActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize">
android:theme="@style/TransparentDialog">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>

View File

@@ -1,89 +0,0 @@
# lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
# https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
# This list contains domains of toxic mastodon instances
# Last-Modified: 1672044500
# gab - a neonazi social network
gab.ai
gab.com
gab.protohype.net
# consequence-free speech
social.unzensiert.to
freeatlantis.com
# reactionary bigotry and hatespeech against marginalized groups
poa.st
freespeechextremist.com
rdrama.cc
outpoa.st
anime.website
gameliberty.club
social.byoblu.com
yggdrasil.social
smuglo.li
dogeposting.social
unsafe.space
freezepeach.xyz
# + CSAM
rojogato.com
# antivaxxer shitposting & fearmongering
shadowsocial.org
# Kiwifarms
kiwifarms.net
kiwifarms.cc
kiwifarms.is
kiwifarms.pleroma.net
# https://mastodon.art/@Curator/109649354849593592
poa.st antisemitic racist homophobic
nicecrew.digital antisemitic
beefyboys.win antisemitic racist homophobic harassment
cawfee.club antisemitic racist homophobic
comfyboy.club antisemitic racist homophobic
freespeechextremist.com racist homophobic
cum.salon racist misogynist
bae.st racist
natehiggers.online racist
rapemeat.solutions misogynist
rapist.town misogynist
rapefeminists.network misogynist
kiwifarms.cc harassment
noagendasocial.com noagenda
posting.lolicon.rocks underage
urchan.org harassment homophobic racist
ryona.agency harassment
yggdrasil.social antisemitic homophobic racist
genderheretics.xyz transphobic
baraag.net underage
lolison.top underage
shota.house underage
shota.social underage
aethy.com underage
taullo.social underage
childpawn.shop underage
posting.lolicon.rocks underage
loli.best underage
gothloli.club underage
smuglo.li underage
youjo.love underage
pedo.school underage
lolison.network underage
freak.university underage
mirr0r.city underage
xhais.love underage
refusal.biz underage
refusal.llc underage
mirr0r.city underage
nnia.space underage
ignorelist.com malicious
repl.co malicious
# custom
pawoo.net csam
1 # lists.d Mastodon Blocklist (c) 2022 Greyhat Academy LICENSED UNDER: CC-BY-NC-SA 4.0
2 # https://raw.githubusercontent.com/greyhat-academy/lists.d/main/mastodon.domains.block.list.tsv
3 # This list contains domains of toxic mastodon instances
4 # Last-Modified: 1672044500
5 # gab - a neonazi social network
6 gab.ai
7 gab.com
8 gab.protohype.net
9 # consequence-free speech
10 social.unzensiert.to
11 freeatlantis.com
12 # reactionary bigotry and hatespeech against marginalized groups
13 poa.st
14 freespeechextremist.com
15 rdrama.cc
16 outpoa.st
17 anime.website
18 gameliberty.club
19 social.byoblu.com
20 yggdrasil.social
21 smuglo.li
22 dogeposting.social
23 unsafe.space
24 freezepeach.xyz
25 # + CSAM
26 rojogato.com
27 # antivaxxer shitposting & fearmongering
28 shadowsocial.org
29 # Kiwifarms
30 kiwifarms.net
31 kiwifarms.cc
32 kiwifarms.is
33 kiwifarms.pleroma.net
34 # https://mastodon.art/@Curator/109649354849593592
35 poa.st antisemitic racist homophobic
36 nicecrew.digital antisemitic
37 beefyboys.win antisemitic racist homophobic harassment
38 cawfee.club antisemitic racist homophobic
39 comfyboy.club antisemitic racist homophobic
40 freespeechextremist.com racist homophobic
41 cum.salon racist misogynist
42 bae.st racist
43 natehiggers.online racist
44 rapemeat.solutions misogynist
45 rapist.town misogynist
46 rapefeminists.network misogynist
47 kiwifarms.cc harassment
48 noagendasocial.com noagenda
49 posting.lolicon.rocks underage
50 urchan.org harassment homophobic racist
51 ryona.agency harassment
52 yggdrasil.social antisemitic homophobic racist
53 genderheretics.xyz transphobic
54 baraag.net underage
55 lolison.top underage
56 shota.house underage
57 shota.social underage
58 aethy.com underage
59 taullo.social underage
60 childpawn.shop underage
61 posting.lolicon.rocks underage
62 loli.best underage
63 gothloli.club underage
64 smuglo.li underage
65 youjo.love underage
66 pedo.school underage
67 lolison.network underage
68 freak.university underage
69 mirr0r.city underage
70 xhais.love underage
71 refusal.biz underage
72 refusal.llc underage
73 mirr0r.city underage
74 nnia.space underage
75 ignorelist.com malicious
76 repl.co malicious
77 # custom
78 pawoo.net csam

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,83 +0,0 @@
package org.joinmastodon.android;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class DomainManager {
private static final String TAG="DomainManager";
private static final DomainManager instance=new DomainManager();
private String currentDomain = "";
public static DomainManager getInstance(){
return instance;
}
private DomainManager(){
}
public String getCurrentDomain() {
return currentDomain;
}
public void setCurrentDomain(String domain) {
this.currentDomain = domain;
}
}

View File

@@ -1,9 +1,9 @@
package org.joinmastodon.android; package org.joinmastodon.android;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.ClipData; import android.content.ClipData;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
@@ -12,9 +12,8 @@ import android.widget.Toast;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.internal.StringUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -29,23 +28,21 @@ public class ExternalShareActivity extends FragmentStackActivity{
UiUtils.setUserPreferredTheme(this); UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if(savedInstanceState==null){ if(savedInstanceState==null){
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
boolean isMastodonURL = UiUtils.looksLikeMastodonUrl(text);
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts(); List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if(sessions.isEmpty()){ if(sessions.isEmpty()){
Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.err_not_logged_in, Toast.LENGTH_SHORT).show();
finish(); finish();
}else if(sessions.size()==1 && !isMastodonURL){ }else if(sessions.size()==1){
openComposeFragment(sessions.get(0).getID()); openComposeFragment(sessions.get(0).getID());
}else{ }else{
new AccountSwitcherSheet(this, false, false, isMastodonURL, accountSession -> { getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));
if(accountSession!=null) new M3AlertDialogBuilder(this)
openComposeFragment(accountSession.getID()); .setItems(sessions.stream().map(as->"@"+as.self.username+"@"+as.domain).toArray(String[]::new), (dialog, which)->{
else openComposeFragment(sessions.get(which).getID());
UiUtils.openURL(this, AccountSessionManager.getInstance().getLastActiveAccountID(), text); })
}).show(); .setTitle(R.string.choose_account)
.setOnCancelListener(dialog -> finish())
.show();
} }
} }
} }
@@ -56,17 +53,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
Intent intent=getIntent(); Intent intent=getIntent();
StringBuilder builder=new StringBuilder(); StringBuilder builder=new StringBuilder();
String subject = ""; String subject = "";
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { if (intent.hasExtra(Intent.EXTRA_SUBJECT)) builder.append(subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)).append("\n\n");
subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); if (intent.hasExtra(Intent.EXTRA_TEXT)) builder.append(intent.getStringExtra(Intent.EXTRA_TEXT)).append("\n");
if (!StringUtil.isBlank(subject)) builder.append(subject).append("\n\n");
}
if (intent.hasExtra(Intent.EXTRA_TEXT)) {
String extra = intent.getStringExtra(Intent.EXTRA_TEXT);
if (!StringUtil.isBlank(extra)) {
if (extra.startsWith(subject)) extra = extra.substring(subject.length()).trim();
builder.append(extra).append("\n\n");
}
}
String text=builder.toString(); String text=builder.toString();
List<Uri> mediaUris; List<Uri> mediaUris;
if(Intent.ACTION_SEND.equals(intent.getAction())){ if(Intent.ACTION_SEND.equals(intent.getAction())){
@@ -93,7 +81,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
args.putString("account", accountID); args.putString("account", accountID);
if(!TextUtils.isEmpty(text)) if(!TextUtils.isEmpty(text))
args.putString("prefilledText", text); args.putString("prefilledText", text);
args.putInt("selectionStart", StringUtil.isBlank(subject) ? 0 : subject.length()); if(!subject.isBlank())
args.putInt("selectionEnd", subject.length());
if(mediaUris!=null && !mediaUris.isEmpty()) if(mediaUris!=null && !mediaUris.isEmpty())
args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris)); args.putParcelableArrayList("mediaAttachments", toArrayList(mediaUris));
Fragment fragment=new ComposeFragment(); Fragment fragment=new ComposeFragment();
@@ -108,11 +97,4 @@ public class ExternalShareActivity extends FragmentStackActivity{
return null; return null;
return new ArrayList<>(l); return new ArrayList<>(l);
} }
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
}
} }

View File

@@ -9,14 +9,10 @@ import android.os.Build;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
public class GlobalUserPreferences{ public class GlobalUserPreferences{
public static boolean playGifs; public static boolean playGifs;
@@ -25,7 +21,7 @@ public class GlobalUserPreferences{
public static boolean showReplies; public static boolean showReplies;
public static boolean showBoosts; public static boolean showBoosts;
public static boolean loadNewPosts; public static boolean loadNewPosts;
public static boolean showNewPostsButton; public static boolean showFederatedTimeline;
public static boolean showInteractionCounts; public static boolean showInteractionCounts;
public static boolean alwaysExpandContentWarnings; public static boolean alwaysExpandContentWarnings;
public static boolean disableMarquee; public static boolean disableMarquee;
@@ -36,51 +32,21 @@ public class GlobalUserPreferences{
public static boolean enableDeleteNotifications; public static boolean enableDeleteNotifications;
public static boolean relocatePublishButton; public static boolean relocatePublishButton;
public static boolean reduceMotion; public static boolean reduceMotion;
public static boolean keepOnlyLatestNotification;
public static boolean enableFabAutoHide;
public static boolean disableAltTextReminder;
public static boolean showAltIndicator;
public static boolean showNoAltIndicator;
public static boolean enablePreReleases;
public static boolean prefixRepliesWithRe;
public static boolean bottomEncoding;
public static boolean collapseLongPosts;
public static boolean spectatorMode;
public static boolean autoHideFab;
public static boolean defaultToUnlistedReplies;
public static boolean disableDoubleTapToSwipe;
public static boolean compactReblogReplyLine;
public static boolean confirmBeforeReblog;
public static boolean replyLineAboveHeader;
public static boolean swapBookmarkWithBoostAction;
public static boolean loadRemoteAccountFollowers;
public static boolean mentionRebloggerAutomatically;
public static String publishButtonText; public static String publishButtonText;
public static ThemePreference theme; public static ThemePreference theme;
public static ColorPreference color; public static ColorPreference color;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType(); private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
public static Map<String, List<String>> recentLanguages; public static Map<String, List<String>> recentLanguages;
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport;
public static Set<String> accountsInGlitchMode;
private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType(); private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
public static Map<String, Integer> recentEmojis; public static Map<String, Integer> recentEmojis;
/** private static SharedPreferences getPrefs(){
* Pleroma
*/
public static String replyVisibility;
public static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE); return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
} }
private static <T> T fromJson(String json, Type type, T orElse) { private static <T> T fromJson(String json, Type type, T orElse) {
if (json == null) return orElse;
try { return gson.fromJson(json, type); } try { return gson.fromJson(json, type); }
catch (JsonSyntaxException ignored) { return orElse; } catch (JsonSyntaxException ignored) { return orElse; }
} }
@@ -93,8 +59,8 @@ public class GlobalUserPreferences{
showReplies=prefs.getBoolean("showReplies", true); showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true); showBoosts=prefs.getBoolean("showBoosts", true);
loadNewPosts=prefs.getBoolean("loadNewPosts", true); loadNewPosts=prefs.getBoolean("loadNewPosts", true);
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true); uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", true);
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", false); showFederatedTimeline=prefs.getBoolean("showFederatedTimeline", !BuildConfig.BUILD_TYPE.equals("playRelease"));
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false); showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false); alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false); disableMarquee=prefs.getBoolean("disableMarquee", false);
@@ -102,37 +68,11 @@ public class GlobalUserPreferences{
disableDividers=prefs.getBoolean("disableDividers", true); disableDividers=prefs.getBoolean("disableDividers", true);
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true); relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true); voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", false); enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", true);
reduceMotion=prefs.getBoolean("reduceMotion", false);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
enableFabAutoHide=prefs.getBoolean("enableFabAutoHide", true);
disableAltTextReminder=prefs.getBoolean("disableAltTextReminder", false);
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false);
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
spectatorMode=prefs.getBoolean("spectatorMode", false);
autoHideFab=prefs.getBoolean("autoHideFab", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
defaultToUnlistedReplies=prefs.getBoolean("defaultToUnlistedReplies", false);
disableDoubleTapToSwipe=prefs.getBoolean("disableDoubleTapToSwipe", false);
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
confirmBeforeReblog=prefs.getBoolean("confirmBeforeReblog", false);
swapBookmarkWithBoostAction=prefs.getBoolean("swapBookmarkWithBoostAction", false);
loadRemoteAccountFollowers=prefs.getBoolean("loadRemoteAccountFollowers", true);
mentionRebloggerAutomatically=prefs.getBoolean("mentionRebloggerAutomatically", false);
publishButtonText=prefs.getString("publishButtonText", "");
theme=ThemePreference.values()[prefs.getInt("theme", 0)]; theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>()); recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>());
recentEmojis=fromJson(prefs.getString("recentEmojis", "{}"), recentEmojisType, new HashMap<>()); recentEmojis=fromJson(prefs.getString("recentEmojis", "{}"), recentEmojisType, new HashMap<>());
publishButtonText=prefs.getString("publishButtonText", ""); publishButtonText=prefs.getString("publishButtonText", "");
pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
replyVisibility=prefs.getString("replyVisibility", null);
try { try {
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){ if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
@@ -153,7 +93,7 @@ public class GlobalUserPreferences{
.putBoolean("showReplies", showReplies) .putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts) .putBoolean("showBoosts", showBoosts)
.putBoolean("loadNewPosts", loadNewPosts) .putBoolean("loadNewPosts", loadNewPosts)
.putBoolean("showNewPostsButton", showNewPostsButton) .putBoolean("showFederatedTimeline", showFederatedTimeline)
.putBoolean("trueBlackTheme", trueBlackTheme) .putBoolean("trueBlackTheme", trueBlackTheme)
.putBoolean("showInteractionCounts", showInteractionCounts) .putBoolean("showInteractionCounts", showInteractionCounts)
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings) .putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
@@ -164,34 +104,11 @@ public class GlobalUserPreferences{
.putBoolean("uniformNotificationIcon", uniformNotificationIcon) .putBoolean("uniformNotificationIcon", uniformNotificationIcon)
.putBoolean("enableDeleteNotifications", enableDeleteNotifications) .putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("reduceMotion", reduceMotion) .putBoolean("reduceMotion", reduceMotion)
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putBoolean("enableFabAutoHide", enableFabAutoHide)
.putBoolean("disableAltTextReminder", disableAltTextReminder)
.putBoolean("showAltIndicator", showAltIndicator)
.putBoolean("showNoAltIndicator", showNoAltIndicator)
.putBoolean("enablePreReleases", enablePreReleases)
.putBoolean("prefixRepliesWithRe", prefixRepliesWithRe)
.putBoolean("collapseLongPosts", collapseLongPosts)
.putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab)
.putString("publishButtonText", publishButtonText) .putString("publishButtonText", publishButtonText)
.putBoolean("bottomEncoding", bottomEncoding)
.putBoolean("defaultToUnlistedReplies", defaultToUnlistedReplies)
.putBoolean("disableDoubleTapToSwipe", disableDoubleTapToSwipe)
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
.putBoolean("confirmBeforeReblog", confirmBeforeReblog)
.putBoolean("swapBookmarkWithBoostAction", swapBookmarkWithBoostAction)
.putBoolean("loadRemoteAccountFollowers", loadRemoteAccountFollowers)
.putBoolean("mentionRebloggerAutomatically", mentionRebloggerAutomatically)
.putInt("theme", theme.ordinal()) .putInt("theme", theme.ordinal())
.putString("color", color.name()) .putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages)) .putString("recentLanguages", gson.toJson(recentLanguages))
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
.putString("recentEmojis", gson.toJson(recentEmojis)) .putString("recentEmojis", gson.toJson(recentEmojis))
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
.putString("replyVisibility", replyVisibility)
.apply(); .apply();
} }

View File

@@ -2,10 +2,8 @@ package org.joinmastodon.android;
import android.Manifest; import android.Manifest;
import android.app.Fragment; import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
@@ -18,7 +16,7 @@ import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment; import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment; import org.joinmastodon.android.fragments.onboarding.CustomLoginFragment;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater; import org.joinmastodon.android.updater.GithubSelfUpdater;
@@ -28,7 +26,6 @@ import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
public class MainActivity extends FragmentStackActivity{ public class MainActivity extends FragmentStackActivity{
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState){ protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this); UiUtils.setUserPreferredTheme(this);
@@ -36,19 +33,18 @@ public class MainActivity extends FragmentStackActivity{
if(savedInstanceState==null){ if(savedInstanceState==null){
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){ if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment()); showFragmentClearingBackStack(new CustomLoginFragment());
}else{ }else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo(); AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session; AccountSession session;
Bundle args=new Bundle(); Bundle args=new Bundle();
Intent intent=getIntent(); Intent intent=getIntent();
boolean fromNotification = intent.getBooleanExtra("fromNotification", false); if(intent.getBooleanExtra("fromNotification", false)){
boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){
String accountID=intent.getStringExtra("accountID"); String accountID=intent.getStringExtra("accountID");
try{ try{
session=AccountSessionManager.getInstance().getAccount(accountID); session=AccountSessionManager.getInstance().getAccount(accountID);
if(!hasNotification) args.putString("tab", "notifications"); if(!intent.hasExtra("notification"))
args.putString("tab", "notifications");
}catch(IllegalStateException x){ }catch(IllegalStateException x){
session=AccountSessionManager.getInstance().getLastActiveAccount(); session=AccountSessionManager.getInstance().getLastActiveAccount();
} }
@@ -58,13 +54,13 @@ public class MainActivity extends FragmentStackActivity{
args.putString("account", session.getID()); args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment(); Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args); fragment.setArguments(args);
if(fromNotification && hasNotification){ showFragmentClearingBackStack(fragment);
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification")); Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID()); showFragmentForNotification(notification, session.getID());
} else if (intent.getBooleanExtra("compose", false)){ }else if(intent.getBooleanExtra("compose", false)){
showCompose(); showCompose();
} else { }else{
showFragmentClearingBackStack(fragment);
maybeRequestNotificationsPermission(); maybeRequestNotificationsPermission();
} }
} }
@@ -83,7 +79,6 @@ public class MainActivity extends FragmentStackActivity{
AccountSession accountSession; AccountSession accountSession;
try{ try{
accountSession=AccountSessionManager.getInstance().getAccount(accountID); accountSession=AccountSessionManager.getInstance().getAccount(accountID);
DomainManager.getInstance().setCurrentDomain(accountSession.domain);
}catch(IllegalStateException x){ }catch(IllegalStateException x){
return; return;
} }
@@ -144,38 +139,4 @@ public class MainActivity extends FragmentStackActivity{
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100); requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
} }
} }
/**
* when opening app through a notification: if (thread) fragment "can go back", clear back stack
* and show home fragment. upstream's implementation doesn't require this as it opens home first
* and then immediately switches to the notification's ThreadFragment. this causes a black
* screen in megalodon, for some reason, so i'm working around this that way.
*/
@Override
public void onBackPressed() {
Fragment currentFragment = getFragmentManager().findFragmentById(
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
);
Bundle currentArgs = currentFragment.getArguments();
if (this.fragmentContainers.size() == 1
&& currentArgs != null
&& currentArgs.getBoolean("_can_go_back", false)
&& currentArgs.containsKey("account")) {
Bundle args = new Bundle();
args.putString("account", currentArgs.getString("account"));
args.putString("tab", "notifications");
Fragment fragment=new HomeFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
} else {
super.onBackPressed();
}
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
}
} }

View File

@@ -1,47 +1,30 @@
package org.joinmastodon.android; package org.joinmastodon.android;
import static org.joinmastodon.android.GlobalUserPreferences.getPrefs;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationChannelGroup; import android.app.NotificationChannelGroup;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.opengl.Visibility;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.notifications.GetNotificationByID; import org.joinmastodon.android.api.requests.notifications.GetNotificationByID;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.NotificationAction;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushNotification; 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.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -54,14 +37,9 @@ public class PushNotificationReceiver extends BroadcastReceiver{
private static final String TAG="PushNotificationReceive"; private static final String TAG="PushNotificationReceive";
public static final int NOTIFICATION_ID=178; public static final int NOTIFICATION_ID=178;
private static final String ACTION_KEY_TEXT_REPLY = "ACTION_KEY_TEXT_REPLY";
private static final int SUMMARY_ID = 791;
private static int notificationId;
@Override @Override
public void onReceive(Context context, Intent intent){ public void onReceive(Context context, Intent intent){
UiUtils.setUserPreferredTheme(context);
if(BuildConfig.DEBUG){ if(BuildConfig.DEBUG){
Log.e(TAG, "received: "+intent); Log.e(TAG, "received: "+intent);
Bundle extras=intent.getExtras(); Bundle extras=intent.getExtras();
@@ -91,7 +69,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
} }
String accountID=account.getID(); String accountID=account.getID();
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s); PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
E.post(new NotificationReceivedEvent(accountID, pn.notificationId+""));
new GetNotificationByID(pn.notificationId+"") new GetNotificationByID(pn.notificationId+"")
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
@@ -113,49 +90,10 @@ public class PushNotificationReceiver extends BroadcastReceiver{
Log.w(TAG, "onReceive: invalid push notification format"); Log.w(TAG, "onReceive: invalid push notification format");
} }
} }
if(intent.getBooleanExtra("fromNotificationAction", false)){
String accountID=intent.getStringExtra("accountID");
int notificationId=intent.getIntExtra("notificationId", -1);
if (notificationId >= 0){
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(accountID, notificationId);
}
if(intent.hasExtra("notification")){
org.joinmastodon.android.model.Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
String statusID = null;
String targetAccountID = null;
if(notification.status != null){
statusID = notification.status.id;
}
if(notification.account != null){
targetAccountID = notification.account.id;
}
if (statusID != null || targetAccountID != null) {
AccountSessionManager accountSessionManager = AccountSessionManager.getInstance();
Preferences preferences = accountSessionManager.getAccount(accountID).preferences;
switch (NotificationAction.values()[intent.getIntExtra("notificationAction", 0)]) {
case FAVORITE -> new SetStatusFavorited(statusID, true).exec(accountID);
case BOOKMARK -> new SetStatusBookmarked(statusID, true).exec(accountID);
case BOOST -> new SetStatusReblogged(notification.status.id, true, preferences.postingDefaultVisibility).exec(accountID);
case UNBOOST -> new SetStatusReblogged(notification.status.id, false, preferences.postingDefaultVisibility).exec(accountID);
case REPLY -> handleReplyAction(context, accountID, intent, notification, notificationId, preferences);
case FOLLOW_BACK -> new SetAccountFollowed(notification.account.id, true, true, false).exec(accountID);
default -> Log.w(TAG, "onReceive: Failed to get NotificationAction");
}
}
}else{
Log.e(TAG, "onReceive: Failed to load notification");
}
}
} }
private 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); NotificationManager nm=context.getSystemService(NotificationManager.class);
notificationId=getPrefs().getInt("latestNotificationId", 0);
Account self=AccountSessionManager.getInstance().getAccount(accountID).self; Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain; String accountName="@"+self.username+"@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
Notification.Builder builder; Notification.Builder builder;
@@ -197,26 +135,23 @@ public class PushNotificationReceiver extends BroadcastReceiver{
builder.setContentTitle(pn.title) builder.setContentTitle(pn.title)
.setContentText(pn.body) .setContentText(pn.body)
.setStyle(new Notification.BigTextStyle().bigText(pn.body)) .setStyle(new Notification.BigTextStyle().bigText(pn.body))
.setSmallIcon(R.drawable.ic_ntf_logo) .setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setContentIntent(PendingIntent.getActivity(context, notificationId, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli()) .setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
.setShowWhen(true) .setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL) .setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true) .setAutoCancel(true)
.setColor(context.getColor(R.color.shortcut_icon_background)); .setColor(context.getColor(R.color.shortcut_icon_background));
if(!GlobalUserPreferences.uniformNotificationIcon){
if (!GlobalUserPreferences.uniformNotificationIcon) { switch (pn.notificationType) {
builder.setSmallIcon(switch (pn.notificationType) { case FAVORITE -> builder.setSmallIcon(R.drawable.ic_fluent_star_24_filled);
case FAVORITE -> R.drawable.ic_fluent_star_24_filled; case REBLOG -> builder.setSmallIcon(R.drawable.ic_fluent_arrow_repeat_all_24_filled);
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled; case FOLLOW -> builder.setSmallIcon(R.drawable.ic_fluent_person_add_24_filled);
case FOLLOW -> R.drawable.ic_fluent_person_add_24_filled; case MENTION -> builder.setSmallIcon(R.drawable.ic_fluent_mention_24_filled);
case MENTION -> R.drawable.ic_fluent_mention_24_filled; case POLL -> builder.setSmallIcon(R.drawable.ic_fluent_poll_24_filled);
case POLL -> R.drawable.ic_fluent_poll_24_filled; default -> builder.setSmallIcon(R.drawable.ic_ntf_logo);
case STATUS -> R.drawable.ic_fluent_chat_24_filled; }
case UPDATE -> R.drawable.ic_fluent_history_24_filled; }else{
case REPORT -> R.drawable.ic_fluent_warning_24_filled; builder.setSmallIcon(R.drawable.ic_ntf_logo);
case SIGN_UP -> R.drawable.ic_fluent_person_available_24_filled;
});
} }
if(avatar!=null){ if(avatar!=null){
@@ -225,100 +160,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){ if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){
builder.setSubText(accountName); builder.setSubText(accountName);
} }
nm.notify(accountID, NOTIFICATION_ID, builder.build());
int id = GlobalUserPreferences.keepOnlyLatestNotification ? NOTIFICATION_ID : notificationId++;
getPrefs().edit().putInt("latestNotificationId", notificationId).apply();
if (notification != null){
switch (pn.notificationType){
case MENTION, STATUS -> {
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
builder.addAction(buildReplyAction(context, id, accountID, notification));
}
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.button_favorite), NotificationAction.FAVORITE));
if(GlobalUserPreferences.swapBookmarkWithBoostAction){
if(notification.status.visibility != StatusPrivacy.DIRECT) {
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.button_reblog), NotificationAction.BOOST));
}else{
// This is just so there is a bookmark action if you cannot reblog the toot
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.add_bookmark), NotificationAction.BOOKMARK));
}
} else {
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.add_bookmark), NotificationAction.BOOKMARK));
}
}
case UPDATE -> {
if(notification.status.reblogged)
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.sk_undo_reblog), NotificationAction.UNBOOST));
}
case FOLLOW -> {
builder.addAction(buildNotificationAction(context, id, accountID, notification, context.getString(R.string.follow_back), NotificationAction.FOLLOW_BACK));
}
}
}
nm.notify(accountID, id, builder.build());
}
private Notification.Action buildNotificationAction(Context context, int notificationId, String accountID, org.joinmastodon.android.model.Notification notification, String title, NotificationAction action){
Intent notificationIntent=new Intent(context, PushNotificationReceiver.class);
notificationIntent.putExtra("notificationId", notificationId);
notificationIntent.putExtra("fromNotificationAction", true);
notificationIntent.putExtra("accountID", accountID);
notificationIntent.putExtra("notificationAction", action.ordinal());
notificationIntent.putExtra("notification", Parcels.wrap(notification));
PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, new Random().nextInt(), notificationIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
return new Notification.Action.Builder(null, title, actionPendingIntent).build();
}
private Notification.Action buildReplyAction(Context context, int notificationId, String accountID, org.joinmastodon.android.model.Notification notification){
String replyLabel = context.getResources().getString(R.string.button_reply);
RemoteInput remoteInput = new RemoteInput.Builder(ACTION_KEY_TEXT_REPLY)
.setLabel(replyLabel)
.build();
Intent notificationIntent=new Intent(context, PushNotificationReceiver.class);
notificationIntent.putExtra("notificationId", notificationId);
notificationIntent.putExtra("fromNotificationAction", true);
notificationIntent.putExtra("accountID", accountID);
notificationIntent.putExtra("notificationAction", NotificationAction.REPLY.ordinal());
notificationIntent.putExtra("notification", Parcels.wrap(notification));
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT : PendingIntent.FLAG_UPDATE_CURRENT;
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(context, new Random().nextInt(), notificationIntent,flags);
return new Notification.Action.Builder(null, replyLabel, replyPendingIntent).addRemoteInput(remoteInput).build();
}
private void handleReplyAction(Context context, String accountID, Intent intent, org.joinmastodon.android.model.Notification notification, int notificationId, Preferences preferences) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput == null) {
Log.e(TAG, "handleReplyAction: Could not get reply input");
return;
}
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
CreateStatus.Request req=new CreateStatus.Request();
req.status = input.toString() + "\n\n" + "@" + notification.status.account.acct;
req.language = notification.status.language;
req.visibility = (notification.status.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : notification.status.visibility);
req.inReplyToId = notification.status.id;
if(!notification.status.spoilerText.isEmpty() && GlobalUserPreferences.prefixRepliesWithRe && !notification.status.spoilerText.startsWith("re: ")){
req.spoilerText = "re: " + notification.status.spoilerText;
}
new CreateStatus(req, UUID.randomUUID().toString()).exec(accountID);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
new Notification.Builder(context, accountID+"_"+notification.type) :
new Notification.Builder(context)
.setPriority(Notification.PRIORITY_DEFAULT)
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo)
.setContentText(context.getString(R.string.mo_notification_action_replied, notification.status.account.getDisplayUsername()))
.build();
notificationManager.notify(accountID, notificationId, repliedNotification);
} }
} }

View File

@@ -11,7 +11,7 @@ public class ApiUtils{
//no instance //no instance
} }
public static <E extends Enum<E>> List<String> enumSetToStrings(EnumSet<E> e, Class<E> cls){ public static <E extends Enum<E>> List<String> enumSetToStrings(EnumSet<E> e, Class<E> cls){
return e.stream().map(ev->{ return e.stream().map(ev->{
try{ try{
SerializedName annotation=cls.getField(ev.name()).getAnnotation(SerializedName.class); SerializedName annotation=cls.getField(ev.name()).getAnnotation(SerializedName.class);

View File

@@ -13,11 +13,9 @@ import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
@@ -128,12 +126,11 @@ public class CacheController{
}); });
} }
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<CacheablePaginatedResponse<List<Notification>>> callback){ public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
cancelDelayedClose(); cancelDelayedClose();
databaseThread.postRunnable(()->{ databaseThread.postRunnable(()->{
try{ try{
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(accountID); List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
List<Filter> filters=accountSession.wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
if(!forceReload){ if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase(); SQLiteDatabase db=getOrOpenDatabase();
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all"; String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
@@ -156,19 +153,18 @@ public class CacheController{
result.add(ntf); result.add(ntf);
}while(cursor.moveToNext()); }while(cursor.moveToNext());
String _newMaxID=newMaxID; String _newMaxID=newMaxID;
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true))); uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
return; return;
} }
}catch(IOException x){ }catch(IOException x){
Log.w(TAG, "getNotifications: corrupted notification object in database", x); Log.w(TAG, "getNotifications: corrupted notification object in database", x);
} }
} }
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain); new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(List<Notification> result){ public void onSuccess(List<Notification> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{ callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){ if(ntf.status!=null){
for(Filter filter:filters){ for(Filter filter:filters){
if(filter.matches(ntf.status)){ if(filter.matches(ntf.status)){
@@ -177,7 +173,7 @@ public class CacheController{
} }
} }
return true; return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false)); }).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id));
putNotifications(result, onlyMentions, onlyPosts, maxID==null); putNotifications(result, onlyMentions, onlyPosts, maxID==null);
} }

View File

@@ -12,15 +12,11 @@ import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter; import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter; import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Status;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
@@ -41,39 +37,19 @@ import okhttp3.ResponseBody;
public class MastodonAPIController{ public class MastodonAPIController{
private static final String TAG="MastodonAPIController"; private static final String TAG="MastodonAPIController";
public static final Gson gsonWithoutDeserializer = new GsonBuilder() public static final Gson gson=new GsonBuilder()
.disableHtmlEscaping() .disableHtmlEscaping()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant.class, new IsoInstantTypeAdapter()) .registerTypeAdapter(Instant.class, new IsoInstantTypeAdapter())
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter()) .registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
.create(); .create();
public static final Gson gson = gsonWithoutDeserializer.newBuilder()
.registerTypeAdapter(Status.class, new Status.StatusDeserializer())
.create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController"); private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder().build(); private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
private AccountSession session; private AccountSession session;
private static List<String> badDomains = new ArrayList<>();
static{ static{
thread.start(); thread.start();
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(
MastodonApp.context.getAssets().open("blocks.tsv")
));
String line;
while ((line = reader.readLine()) != null) {
if (line.isBlank() || line.startsWith("#")) continue;
String[] parts = line.replaceAll("\"", "").split("[\s,;]");
if (parts.length == 0) continue;
String domain = parts[0].toLowerCase().trim();
if (domain.isBlank()) continue;
badDomains.add(domain);
}
} catch (IOException e) {
e.printStackTrace();
}
} }
public MastodonAPIController(@Nullable AccountSession session){ public MastodonAPIController(@Nullable AccountSession session){
@@ -81,11 +57,8 @@ public class MastodonAPIController{
} }
public <T> void submitRequest(final MastodonAPIRequest<T> req){ public <T> void submitRequest(final MastodonAPIRequest<T> req){
final String host = req.getURL().getHost();
final boolean isBad = host == null || badDomains.stream().anyMatch(h -> h.equalsIgnoreCase(host) || host.toLowerCase().endsWith("." + h));
thread.postRunnable(()->{ thread.postRunnable(()->{
try{ try{
// if (isBad) throw new IllegalArgumentException();
if(req.canceled) if(req.canceled)
return; return;
Request.Builder builder=new Request.Builder() Request.Builder builder=new Request.Builder()

View File

@@ -5,6 +5,8 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.R;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{ public class MastodonErrorResponse extends ErrorResponse{
@@ -20,7 +22,7 @@ public class MastodonErrorResponse extends ErrorResponse{
@Override @Override
public void bindErrorView(View view){ public void bindErrorView(View view){
TextView text=view.findViewById(me.grishka.appkit.R.id.error_text); TextView text=view.findViewById(R.id.error_text);
text.setText(error); text.setText(error);
} }

View File

@@ -162,8 +162,6 @@ public class PushSubscriptionManager{
@Override @Override
public void onSuccess(PushSubscription result){ public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{ MastodonAPIController.runInBackground(()->{
result.serverKey=result.serverKey.replace('/','_');
result.serverKey=result.serverKey.replace('+','-');
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE)); serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID); AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
@@ -372,7 +370,7 @@ public class PushSubscriptionManager{
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){ for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
if(session.pushSubscription==null || forceReRegister) if(session.pushSubscription==null || forceReRegister)
session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription); session.getPushSubscriptionManager().registerAccountForPush(session.pushSubscription);
else else if(session.needUpdatePushSettings)
session.getPushSubscriptionManager().updatePushSettings(session.pushSubscription); session.getPushSubscriptionManager().updatePushSettings(session.pushSubscription);
} }
} }

View File

@@ -1,11 +0,0 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
public class GetAccountByHandle extends MastodonAPIRequest<Account>{
public GetAccountByHandle(String acct){
super(HttpMethod.GET, "/accounts/lookup", Account.class);
addQueryParameter("acct", acct);
}
}

View File

@@ -4,10 +4,6 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{ public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
this(id, followed, showReblogs, false);
}
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){ public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class); super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
if(followed) if(followed)

View File

@@ -4,15 +4,8 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
public class SetAccountMuted extends MastodonAPIRequest<Relationship>{ public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
public SetAccountMuted(String id, boolean muted, long duration){ public SetAccountMuted(String id, boolean muted){
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class); super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
setRequestBody(muted ? new Request(duration): new Object()); setRequestBody(new Object());
}
private static class Request{
public long duration;
public Request(long duration){
this.duration=duration;
}
} }
} }

View File

@@ -1,19 +0,0 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
public class SetPrivateNote extends MastodonAPIRequest<Relationship>{
public SetPrivateNote(String id, String comment){
super(MastodonAPIRequest.HttpMethod.POST, "/accounts/"+id+"/note", Relationship.class);
Request req = new Request(comment);
setRequestBody(req);
}
private static class Request{
public String comment;
public Request(String comment){
this.comment=comment;
}
}
}

View File

@@ -1,10 +0,0 @@
package org.joinmastodon.android.api.requests.announcements;
import org.joinmastodon.android.api.MastodonAPIRequest;
public class DismissAnnouncement extends MastodonAPIRequest<Object>{
public DismissAnnouncement(String id){
super(HttpMethod.POST, "/announcements/" + id + "/dismiss", Object.class);
setRequestBody(new Object());
}
}

View File

@@ -1,15 +0,0 @@
package org.joinmastodon.android.api.requests.announcements;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Announcement;
import java.util.List;
public class GetAnnouncements extends MastodonAPIRequest<List<Announcement>> {
public GetAnnouncements(boolean withDismissed) {
super(MastodonAPIRequest.HttpMethod.GET, "/announcements", new TypeToken<>(){});
addQueryParameter("with_dismissed", withDismissed ? "true" : "false");
}
}

View File

@@ -1,19 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
public class CreateList extends MastodonAPIRequest<ListTimeline> {
public CreateList(String title, ListTimeline.RepliesPolicy repliesPolicy) {
super(HttpMethod.POST, "/lists", ListTimeline.class);
Request req = new Request();
req.title = title;
req.repliesPolicy = repliesPolicy;
setRequestBody(req);
}
public static class Request {
public String title;
public ListTimeline.RepliesPolicy repliesPolicy;
}
}

View File

@@ -1,10 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
public class DeleteList extends MastodonAPIRequest<Object> {
public DeleteList(String id) {
super(HttpMethod.DELETE, "/lists/" + id, Object.class);
}
}

View File

@@ -1,10 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
public class GetList extends MastodonAPIRequest<ListTimeline> {
public GetList(String id) {
super(HttpMethod.GET, "/lists/" + id, ListTimeline.class);
}
}

View File

@@ -1,14 +0,0 @@
package org.joinmastodon.android.api.requests.lists;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ListTimeline;
public class UpdateList extends MastodonAPIRequest<ListTimeline> {
public UpdateList(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
super(HttpMethod.PUT, "/lists/" + id, ListTimeline.class);
CreateList.Request req = new CreateList.Request();
req.title = title;
req.repliesPolicy = repliesPolicy;
setRequestBody(req);
}
}

View File

@@ -1,17 +0,0 @@
package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.Markers;
import java.util.EnumSet;
public class GetMarkers extends MastodonAPIRequest<Markers> {
public GetMarkers(EnumSet<Marker.Type> timelines) {
super(HttpMethod.GET, "/markers", Markers.class);
for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){
addQueryParameter("timeline[]", type);
}
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.api.requests.notifications; package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils; import org.joinmastodon.android.api.ApiUtils;
@@ -10,25 +11,19 @@ import java.util.EnumSet;
import java.util.List; import java.util.List;
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{ public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, boolean isPleromaInstance){ public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
super(HttpMethod.GET, "/notifications", new TypeToken<>(){}); super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
if(maxID!=null) if(maxID!=null)
addQueryParameter("max_id", maxID); addQueryParameter("max_id", maxID);
if(limit>0) if(limit>0)
addQueryParameter("limit", ""+limit); addQueryParameter("limit", ""+limit);
if(includeTypes!=null){ if(includeTypes!=null){
if(!isPleromaInstance) {
for(String type:ApiUtils.enumSetToStrings(includeTypes, Notification.Type.class)){ for(String type:ApiUtils.enumSetToStrings(includeTypes, Notification.Type.class)){
addQueryParameter("types[]", type); addQueryParameter("types[]", type);
} }
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), Notification.Type.class)){ for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), Notification.Type.class)){
addQueryParameter("exclude_types[]", type); addQueryParameter("exclude_types[]", type);
} }
}else{
for(String type:ApiUtils.enumSetToStrings(includeTypes, Notification.Type.class)){
addQueryParameter("include_types[]", type);
}
}
} }
removeUnsupportedItems=true; removeUnsupportedItems=true;
} }

View File

@@ -9,7 +9,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
Request r=new Request(); Request r=new Request();
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID; r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
r.data.alerts=alerts; r.data.alerts=alerts;
r.policy=policy; r.data.policy=policy;
r.subscription.keys.p256dh=encryptionKey; r.subscription.keys.p256dh=encryptionKey;
r.subscription.keys.auth=authKey; r.subscription.keys.auth=authKey;
setRequestBody(r); setRequestBody(r);
@@ -18,7 +18,6 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Request{ private static class Request{
public Subscription subscription=new Subscription(); public Subscription subscription=new Subscription();
public Data data=new Data(); public Data data=new Data();
public PushSubscription.Policy policy;
private static class Keys{ private static class Keys{
public String p256dh; public String p256dh;
@@ -32,6 +31,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Data{ private static class Data{
public PushSubscription.Alerts alerts; public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
} }
} }
} }

View File

@@ -3,36 +3,23 @@ package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
import java.io.IOException;
import okhttp3.Response;
public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{ public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{
private final PushSubscription.Policy policy;
public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){ public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
super(HttpMethod.PUT, "/push/subscription", PushSubscription.class); super(HttpMethod.PUT, "/push/subscription", PushSubscription.class);
setRequestBody(new Request(alerts, policy)); setRequestBody(new Request(alerts, policy));
this.policy=policy;
}
@Override
public void validateAndPostprocessResponse(PushSubscription respObj, Response httpResponse) throws IOException{
super.validateAndPostprocessResponse(respObj, httpResponse);
respObj.policy=policy;
} }
private static class Request{ private static class Request{
public Data data=new Data(); public Data data=new Data();
public PushSubscription.Policy policy;
public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){ public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
this.data.alerts=alerts; this.data.alerts=alerts;
this.policy=policy; this.data.policy=policy;
} }
private static class Data{ private static class Data{
public PushSubscription.Alerts alerts; public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
} }
} }
} }

View File

@@ -39,14 +39,11 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public Poll poll; public Poll poll;
public String inReplyToId; public String inReplyToId;
public boolean sensitive; public boolean sensitive;
public boolean localOnly;
public String spoilerText; public String spoilerText;
public StatusPrivacy visibility; public StatusPrivacy visibility;
public Instant scheduledAt; public Instant scheduledAt;
public String language; public String language;
public String quoteId;
public static class Poll{ public static class Poll{
public ArrayList<String> options=new ArrayList<>(); public ArrayList<String> options=new ArrayList<>();
public int expiresIn; public int expiresIn;

View File

@@ -9,10 +9,6 @@ import org.joinmastodon.android.model.Hashtag;
import java.util.List; import java.util.List;
public class GetFollowedHashtags extends HeaderPaginationRequest<Hashtag> { public class GetFollowedHashtags extends HeaderPaginationRequest<Hashtag> {
public GetFollowedHashtags() {
this(null, null, -1, null);
}
public GetFollowedHashtags(String maxID, String minID, int limit, String sinceID){ public GetFollowedHashtags(String maxID, String minID, int limit, String sinceID){
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){}); super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
if(maxID!=null) if(maxID!=null)

View File

@@ -2,7 +2,6 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -19,7 +18,5 @@ public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("since_id", sinceID); addQueryParameter("since_id", sinceID);
if(limit>0) if(limit>0)
addQueryParameter("limit", ""+limit); addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
} }
} }

View File

@@ -8,11 +8,9 @@ import org.joinmastodon.android.model.Status;
import java.util.List; import java.util.List;
public class GetTrendingStatuses extends MastodonAPIRequest<List<Status>>{ public class GetTrendingStatuses extends MastodonAPIRequest<List<Status>>{
public GetTrendingStatuses(int offset, int limit){ public GetTrendingStatuses(int limit){
super(HttpMethod.GET, "/trends/statuses", new TypeToken<>(){}); super(HttpMethod.GET, "/trends/statuses", new TypeToken<>(){});
if(limit>0) if(limit>0)
addQueryParameter("limit", ""+limit); addQueryParameter("limit", ""+limit);
if(offset>0)
addQueryParameter("offset", ""+offset);
} }
} }

View File

@@ -7,7 +7,6 @@ import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
@@ -32,7 +31,6 @@ public class AccountSession{
public String pushAccountID; public String pushAccountID;
public Preferences preferences; public Preferences preferences;
public AccountActivationInfo activationInfo; public AccountActivationInfo activationInfo;
public Markers markers;
private transient MastodonAPIController apiController; private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController; private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
private transient CacheController cacheController; private transient CacheController cacheController;

View File

@@ -25,7 +25,6 @@ import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent; import org.joinmastodon.android.events.EmojiUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
@@ -34,8 +33,6 @@ import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
@@ -49,7 +46,6 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -65,7 +61,7 @@ import me.grishka.appkit.api.ErrorResponse;
public class AccountSessionManager{ public class AccountSessionManager{
private static final String TAG="AccountSessionManager"; private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push"; public static final String SCOPE="read write follow push";
public static final String REDIRECT_URI = getRedirectURI(); public static final String REDIRECT_URI="moshidon-android-auth://callback";
private static final AccountSessionManager instance=new AccountSessionManager(); private static final AccountSessionManager instance=new AccountSessionManager();
@@ -84,17 +80,6 @@ public class AccountSessionManager{
return instance; return instance;
} }
public static String getRedirectURI() {
StringBuilder builder = new StringBuilder();
builder.append("moshidon-android-");
if (BuildConfig.BUILD_TYPE.equals("debug") || BuildConfig.BUILD_TYPE.equals("nightly")) {
builder.append(BuildConfig.BUILD_TYPE);
builder.append('-');
}
builder.append("auth://callback");
return builder.toString();
}
private AccountSessionManager(){ private AccountSessionManager(){
prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE); prefs=MastodonApp.context.getSharedPreferences("account_manager", Context.MODE_PRIVATE);
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json"); File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
@@ -226,7 +211,7 @@ public class AccountSessionManager{
.path("/oauth/authorize") .path("/oauth/authorize")
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.appendQueryParameter("client_id", result.clientId) .appendQueryParameter("client_id", result.clientId)
.appendQueryParameter("redirect_uri", REDIRECT_URI) .appendQueryParameter("redirect_uri", "moshidon-android-auth://callback")
.appendQueryParameter("scope", SCOPE) .appendQueryParameter("scope", SCOPE)
.build(); .build();
@@ -270,7 +255,6 @@ public class AccountSessionManager{
// if(now-session.filtersLastUpdated>3600_000L){ // if(now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session); updateSessionWordFilters(session);
// } // }
updateSessionMarkers(session);
} }
if(loadedInstances){ if(loadedInstances){
maybeUpdateCustomEmojis(domains); maybeUpdateCustomEmojis(domains);
@@ -287,15 +271,6 @@ public class AccountSessionManager{
} }
} }
private void preferencesFromSource(AccountSession session, Account account) {
if (account != null && account.source != null && session.preferences != null) {
if (account.source.privacy != null)
session.preferences.postingDefaultVisibility = account.source.privacy;
if (account.source.language != null)
session.preferences.postingDefaultLanguage = account.source.language;
}
}
private void updateSessionLocalInfo(AccountSession session){ private void updateSessionLocalInfo(AccountSession session){
new GetOwnAccount() new GetOwnAccount()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@@ -303,12 +278,13 @@ public class AccountSessionManager{
public void onSuccess(Account result){ public void onSuccess(Account result){
session.self=result; session.self=result;
session.infoLastUpdated=System.currentTimeMillis(); session.infoLastUpdated=System.currentTimeMillis();
preferencesFromSource(session, result);
writeAccountsFile(); writeAccountsFile();
} }
@Override @Override
public void onError(ErrorResponse error){} public void onError(ErrorResponse error){
}
}) })
.exec(session.getID()); .exec(session.getID());
} }
@@ -318,14 +294,10 @@ public class AccountSessionManager{
@Override @Override
public void onSuccess(Preferences preferences) { public void onSuccess(Preferences preferences) {
session.preferences=preferences; session.preferences=preferences;
preferencesFromSource(session, session.self);
} }
@Override @Override
public void onError(ErrorResponse error) { public void onError(ErrorResponse error) {}
session.preferences = new Preferences();
preferencesFromSource(session, session.self);
}
}).exec(session.getID()); }).exec(session.getID());
} }
@@ -347,21 +319,6 @@ public class AccountSessionManager{
.exec(session.getID()); .exec(session.getID());
} }
private void updateSessionMarkers(AccountSession session) {
new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() {
@Override
public void onSuccess(Markers markers) {
session.markers = markers;
writeAccountsFile();
}
@Override
public void onError(ErrorResponse error) {
}
}).exec(session.getID());
}
public void updateInstanceInfo(String domain){ public void updateInstanceInfo(String domain){
new GetInstance() new GetInstance()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){

View File

@@ -1,4 +0,0 @@
package org.joinmastodon.android.events;
public class AllNotificationsSeenEvent {
}

View File

@@ -1,11 +0,0 @@
package org.joinmastodon.android.events;
public class HashtagUpdatedEvent {
public final String name;
public final boolean following;
public HashtagUpdatedEvent(String name, boolean following) {
this.name = name;
this.following = following;
}
}

View File

@@ -1,9 +0,0 @@
package org.joinmastodon.android.events;
public class ListDeletedEvent {
public final String id;
public ListDeletedEvent(String id) {
this.id = id;
}
}

View File

@@ -1,15 +0,0 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ListTimeline;
public class ListUpdatedCreatedEvent {
public final String id;
public final String title;
public final ListTimeline.RepliesPolicy repliesPolicy;
public ListUpdatedCreatedEvent(String id, String title, ListTimeline.RepliesPolicy repliesPolicy) {
this.id = id;
this.title = title;
this.repliesPolicy = repliesPolicy;
}
}

View File

@@ -1,9 +0,0 @@
package org.joinmastodon.android.events;
public class NotificationReceivedEvent {
public String account, id;
public NotificationReceivedEvent(String account, String id) {
this.account = account;
this.id = id;
}
}

View File

@@ -3,10 +3,6 @@ package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.animation.TranslateAnimation;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
@@ -15,15 +11,12 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
@@ -63,8 +56,8 @@ public class AccountTimelineFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(List<Status> result){ public void onSuccess(List<Status> result){
if(getActivity()==null) return; if(getActivity()==null)
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList()); return;
onDataLoaded(result, !result.isEmpty()); onDataLoaded(result, !result.isEmpty());
} }
}) })
@@ -74,7 +67,6 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
fab = ((ProfileFragment) getParentFragment()).getFab();
} }
@Override @Override

View File

@@ -1,106 +0,0 @@
package org.joinmastodon.android.fragments;
import static java.util.stream.Collectors.toList;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetScheduledStatuses;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.PaginatedList;
import me.grishka.appkit.api.SimpleCallback;
public class AnnouncementsFragment extends BaseStatusListFragment<Announcement> {
private Instance instance;
private AccountSession session;
private List<String> unreadIDs = null;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.sk_announcements);
session = AccountSessionManager.getInstance().getAccount(accountID);
instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
loadData();
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Announcement a) {
if(TextUtils.isEmpty(a.content)) return List.of();
Account instanceUser = new Account();
instanceUser.id = instanceUser.acct = instanceUser.username = session.domain;
instanceUser.displayName = instance.title;
instanceUser.url = "https://"+session.domain+"/about";
instanceUser.avatar = instanceUser.avatarStatic = instance.thumbnail;
instanceUser.emojis = List.of();
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;
return List.of(
HeaderStatusDisplayItem.fromAnnouncement(a, fakeStatus, instanceUser, this, accountID, this::onMarkAsRead),
textItem
);
}
public void onMarkAsRead(String id) {
if (unreadIDs == null) return;
unreadIDs.remove(id);
if (unreadIDs.isEmpty()) setResult(true, null);
}
@Override
protected void addAccountToKnown(Announcement s) {}
@Override
public void onItemClick(String id) {}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAnnouncements(true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Announcement> result){
if (getActivity() == null) return;
List<Announcement> unread = result.stream().filter(a -> !a.read).collect(toList());
List<Announcement> read = result.stream().filter(a -> a.read).collect(toList());
onDataLoaded(unread, true);
onDataLoaded(read, false);
if (unread.isEmpty()) setResult(true, null);
else unreadIDs = unread.stream().map(a -> a.id).collect(toList());
}
})
.exec(accountID);
}
}

View File

@@ -13,22 +13,16 @@ import android.text.Layout;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.DomainManager;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote; import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.DisplayItemsParent;
@@ -36,20 +30,20 @@ import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.TileGridLayoutManager;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.TypedObjectPool; import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -60,9 +54,8 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment;
@@ -73,33 +66,22 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, DomainDisplay{ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop{
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>(); protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter; protected DisplayItemsAdapter adapter;
protected String accountID; protected String accountID;
protected PhotoViewer currentPhotoViewer; protected PhotoViewer currentPhotoViewer;
protected ImageButton fab;
protected int scrollDiff = 0;
protected HashMap<String, Account> knownAccounts=new HashMap<>(); protected HashMap<String, Account> knownAccounts=new HashMap<>();
protected HashMap<String, Relationship> relationships=new HashMap<>(); protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect(); protected Rect tmpRect=new Rect();
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
private final int THRESHOLD = 800;
public BaseStatusListFragment(){ public BaseStatusListFragment(){
super(20); super(20);
if (wantsComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab);
}
protected boolean wantsComposeButton() {
return false;
} }
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
UiUtils.loadMaxWidth(getContext());
if(GlobalUserPreferences.disableMarquee){ if(GlobalUserPreferences.disableMarquee){
setTitleMarqueeEnabled(false); setTitleMarqueeEnabled(false);
setSubtitleMarqueeEnabled(false); setSubtitleMarqueeEnabled(false);
@@ -194,21 +176,21 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
@Override @Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){ public void openPhotoViewer(String parentID, Status _status, int attachmentIndex){
final Status status=_status.getContentStatus(); final Status status=_status.reblog!=null ? _status.reblog : _status;
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){ currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
private MediaAttachmentViewController transitioningHolder; private ImageStatusDisplayItem.Holder<?> transitioningHolder;
@Override @Override
public void setPhotoViewVisibility(int index, boolean visible){ public void setPhotoViewVisibility(int index, boolean visible){
MediaAttachmentViewController holder=findPhotoViewHolder(index); ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
if(holder!=null) if(holder!=null)
holder.photo.setAlpha(visible ? 1f : 0f); holder.photo.setAlpha(visible ? 1f : 0f);
} }
@Override @Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
MediaAttachmentViewController holder=findPhotoViewHolder(index); ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
if(holder!=null){ if(holder!=null){
transitioningHolder=holder; transitioningHolder=holder;
View view=transitioningHolder.photo; View view=transitioningHolder.photo;
@@ -216,8 +198,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
view.getLocationOnScreen(pos); view.getLocationOnScreen(pos);
outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight()); outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight());
list.setClipChildren(false); list.setClipChildren(false);
gridHolder.setClipChildren(false); transitioningHolder.itemView.setElevation(1f);
transitioningHolder.view.setElevation(1f);
return true; return true;
} }
return false; return false;
@@ -244,16 +225,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
view.setTranslationY(0f); view.setTranslationY(0f);
view.setScaleX(1f); view.setScaleX(1f);
view.setScaleY(1f); view.setScaleY(1f);
transitioningHolder.view.setElevation(0f); transitioningHolder.itemView.setElevation(0f);
if(list!=null) if(list!=null)
list.setClipChildren(true); list.setClipChildren(true);
gridHolder.setClipChildren(true);
transitioningHolder=null; transitioningHolder=null;
} }
@Override @Override
public Drawable getPhotoViewCurrentDrawable(int index){ public Drawable getPhotoViewCurrentDrawable(int index){
MediaAttachmentViewController holder=findPhotoViewHolder(index); ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index);
if(holder!=null) if(holder!=null)
return holder.photo.getDrawable(); return holder.photo.getDrawable();
return null; return null;
@@ -269,66 +249,35 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST); requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
} }
private MediaAttachmentViewController findPhotoViewHolder(int index){ private ImageStatusDisplayItem.Holder<?> findPhotoViewHolder(int index){
return gridHolder.getViewController(index); if(list==null)
return null;
int offset=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(parentID)){
if(item instanceof ImageStatusDisplayItem){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(getMainAdapterOffset()+offset+index);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
return imgHolder;
}
return null;
}
}
offset++;
}
return null;
} }
}); });
} }
public @Nullable View getFab() {
if (getParentFragment() instanceof HasFab l) return l.getFab();
else return fab;
}
public void animateFab(boolean show) {
View fab = getFab();
if (fab == null) return;
if (show && fab.getVisibility() != View.VISIBLE) {
fab.setVisibility(View.VISIBLE);
TranslateAnimation animate = new TranslateAnimation(
0,
0,
fab.getHeight() * 2,
0);
animate.setDuration(300);
fab.startAnimation(animate);
} else if (!show && fab.getVisibility() == View.VISIBLE) {
TranslateAnimation animate = new TranslateAnimation(
0,
0,
0,
fab.getHeight() * 2);
animate.setDuration(300);
fab.startAnimation(animate);
fab.setVisibility(View.INVISIBLE);
scrollDiff = 0;
}
}
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){ list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override @Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(currentPhotoViewer!=null) if(currentPhotoViewer!=null)
currentPhotoViewer.offsetView(-dx, -dy); currentPhotoViewer.offsetView(-dx, -dy);
View fab = getFab();
if (fab!=null && GlobalUserPreferences.enableFabAutoHide) {
if (dy > 0 && fab.getVisibility() == View.VISIBLE) {
animateFab(false);
} else if (dy < 0 && fab.getVisibility() != View.VISIBLE) {
if (list.getChildAt(0).getTop() == 0 || scrollDiff > THRESHOLD) {
animateFab(true);
scrollDiff = 0;
} else {
scrollDiff += Math.abs(dy);
}
}
}
} }
}); });
list.addItemDecoration(new StatusListItemDecoration()); list.addItemDecoration(new StatusListItemDecoration());
@@ -364,14 +313,31 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
list.setItemAnimator(new BetterItemAnimator()); list.setItemAnimator(new BetterItemAnimator());
((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true); ((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true);
updateToolbar(); updateToolbar();
if (wantsComposeButton() && !getArguments().getBoolean("__disable_fab", false)) {
fab.setVisibility(View.VISIBLE);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(this::onFabLongClick);
} else if (fab != null) {
fab.setVisibility(View.GONE);
} }
@Override
protected RecyclerView.LayoutManager onCreateLayoutManager(){
GridLayoutManager lm=new TileGridLayoutManager(getActivity(), 1000);
lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position){
position-=getMainAdapterOffset();
if(position>=0 && position<displayItems.size()){
StatusDisplayItem item=displayItems.get(position);
if(item instanceof ImageStatusDisplayItem imgItem){
PhotoLayoutHelper.TiledLayoutResult layout=imgItem.tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgItem.thisTile;
int spans=0;
for(int i=0;i<tile.colSpan;i++){
spans+=layout.columnSizes[tile.startCol+i];
}
return spans;
}
}
return 1000;
}
});
return lm;
} }
@Override @Override
@@ -422,7 +388,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1); List<StatusDisplayItem> pollItems=displayItems.subList(firstOptionIndex, footerIndex+1);
int prevSize=pollItems.size(); int prevSize=pollItems.size();
pollItems.clear(); pollItems.clear();
StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems, status); StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems);
if(prevSize!=pollItems.size()){ if(prevSize!=pollItems.size()){
adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize); adapter.notifyItemRangeRemoved(firstOptionIndex, prevSize);
adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size()); adapter.notifyItemRangeInserted(firstOptionIndex, pollItems.size());
@@ -459,26 +425,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
} }
}else{ }else{
if(holder.getItem().status.reloadWhenClicked){
Status queryStatus = holder.getItem().status;
UiUtils.lookupStatus(getContext(), queryStatus, accountID, null, status -> {
submitPollVote(holder.getItemID(), status.poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
});
return;
}
submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option))); submitPollVote(holder.getItemID(), poll.id, Collections.singletonList(poll.options.indexOf(option)));
} }
} }
public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){ public void onPollVoteButtonClick(PollFooterStatusDisplayItem.Holder holder){
Poll poll=holder.getItem().poll; Poll poll=holder.getItem().poll;
if(holder.getItem().status.reloadWhenClicked){
Status queryStatus = holder.getItem().status;
UiUtils.lookupStatus(getContext(), queryStatus, accountID, null, status -> {
submitPollVote(holder.getItemID(), status.poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
});
return;
}
submitPollVote(holder.getItemID(), poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList())); submitPollVote(holder.getItemID(), poll.id, poll.selectedOptions.stream().map(opt->poll.options.indexOf(opt)).collect(Collectors.toList()));
} }
@@ -506,7 +458,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
revealSpoiler(status, holder.getItemID()); revealSpoiler(status, holder.getItemID());
} }
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){ public void onRevealSpoilerClick(ImageStatusDisplayItem.Holder<?> holder){
Status status=holder.getItem().status; Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID()); revealSpoiler(status, holder.getItemID());
} }
@@ -526,7 +478,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
Status status=holder.getItem().status; Status status=holder.getItem().status;
status.spoilerRevealed=!status.spoilerRevealed; status.spoilerRevealed=!status.spoilerRevealed;
if(!TextUtils.isEmpty(status.spoilerText)){ if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text = findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class); TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){ if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()); adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
} }
@@ -535,32 +487,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
updateImagesSpoilerState(status, holder.getItemID()); updateImagesSpoilerState(status, holder.getItemID());
} }
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, String itemID) {
status.textExpanded = !status.textExpanded;
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();
}
protected void updateImagesSpoilerState(Status status, String itemID){ protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>(); ArrayList<Integer> updatedPositions=new ArrayList<>();
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class); for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
if(mediaGrid!=null){ photo.setRevealed(status.spoilerRevealed);
mediaGrid.setRevealed(status.spoilerRevealed); updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset());
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset());
} }
int i=0; int i=0;
for(StatusDisplayItem item:displayItems){ for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){ if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i); adapter.notifyItemChanged(i);
} }
i++; i++;
@@ -569,15 +504,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onGapClick(GapStatusDisplayItem.Holder item){} public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
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;
}
public String getAccountID(){ public String getAccountID(){
return accountID; return accountID;
} }
@@ -647,11 +573,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0) == null || list.getChildAt(0).getTop() == 0;
}
protected int getListWidthForMediaLayout(){ protected int getListWidthForMediaLayout(){
return list.getWidth(); return list.getWidth();
} }
@@ -698,25 +619,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer.onPause(); currentPhotoViewer.onPause();
} }
public void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);
}
public boolean onFabLongClick(View v) {
return UiUtils.pickAccountForCompose(getActivity(), accountID);
}
private MediaAttachmentViewController makeNewMediaAttachmentView(MediaGridStatusDisplayItem.GridItemType type){
return new MediaAttachmentViewController(getActivity(), type);
}
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> getAttachmentViewsPool(){
return attachmentViewsPool;
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){ public DisplayItemsAdapter(){
@@ -754,6 +656,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public ImageLoaderRequest getImageRequest(int position, int image){ public ImageLoaderRequest getImageRequest(int position, int image){
return displayItems.get(position).getImageRequest(image); return displayItems.get(position).getImageRequest(image);
} }
// @Override
// public void onViewDetachedFromWindow(@NonNull BindableViewHolder<StatusDisplayItem> holder){
// if(holder instanceof ImageLoaderViewHolder){
// int count=holder.getItem().getImageCount();
// for(int i=0;i<count;i++){
// ((ImageLoaderViewHolder) holder).clearImage(i);
// }
// }
// }
} }
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{ private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
@@ -787,21 +699,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<parent.getChildCount();i++){ for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i); View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child); RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){ if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
hiddenMediaPaint.setColor(0x80000000); hiddenMediaPaint.setColor(0x80000000);
c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint); PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
float hGap=tile.startCol>0 ? V.dp(1) : 0;
float vGap=tile.startRow>0 ? V.dp(1) : 0;
c.drawRect(child.getX()-hGap, child.getY()-vGap, child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
} }
} }
} }
for(int i=0;i<parent.getChildCount();i++){ for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i); View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child); RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){ if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed){ if(!imgHolder.getItem().status.spoilerRevealed){
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
if(tile.startCol==0 && tile.startRow==0 && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout(); int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, UiUtils.MAX_WIDTH); int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH));
if(currentMediaHiddenLayoutsWidth!=width) if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32)); rebuildMediaHiddenLayouts(width-V.dp(32));
c.save(); c.save();
@@ -826,6 +742,47 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
} }
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof ImageStatusDisplayItem.Holder){
int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH));
PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem.Holder<?>) holder).getItem().tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem.Holder<?>) holder).getItem().thisTile;
if(tile.startCol+tile.colSpan<layout.columnSizes.length){
outRect.right=V.dp(1);
}
if(tile.startRow+tile.rowSpan<layout.rowSizes.length){
outRect.bottom=V.dp(1);
}
// For a view that spans rows, compensate its additional height so the row it's in stays the right height
if(tile.rowSpan>1){
outRect.bottom=-(Math.round(tile.height/1000f*width)-Math.round(layout.rowSizes[tile.startRow]/1000f*width));
}
// ...and for its siblings, offset those on rows below first to the right where they belong
if(tile.startCol>0 && layout.tiles[0].rowSpan>1 && tile.startRow>layout.tiles[0].startRow){
int xOffset=Math.round(layout.tiles[0].width/1000f*listWidth);
outRect.left=xOffset;
outRect.right=-xOffset;
}
// If the width of the media block is smaller than that of the RecyclerView, offset the views horizontally to center them
if(listWidth>width){
outRect.left+=(listWidth-V.dp(ImageAttachmentFrameLayout.MAX_WIDTH))/2;
if(tile.startCol>0){
int spanOffset=0;
for(int i=0;i<tile.startCol;i++){
spanOffset+=layout.columnSizes[i];
}
outRect.left-=Math.round(spanOffset/1000f*listWidth);
outRect.left+=Math.round(spanOffset/1000f*width);
}
}
}
}
private void rebuildMediaHiddenLayouts(int width){ private void rebuildMediaHiddenLayouts(int width){
currentMediaHiddenLayoutsWidth=width; currentMediaHiddenLayoutsWidth=width;
String title=getString(R.string.sensitive_content); String title=getString(R.string.sensitive_content);

View File

@@ -25,7 +25,6 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(HeaderPaginationList<Status> result){ public void onSuccess(HeaderPaginationList<Status> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null) if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id"); nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else else

View File

@@ -1,11 +1,8 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import static android.os.ext.SdkExtensions.getExtensionVersion;
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages; import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT; import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant; import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
import static org.joinmastodon.android.ui.utils.UiUtils.isPhotoPickerAvailable;
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages; import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages; import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
@@ -29,11 +26,9 @@ import android.graphics.drawable.LayerDrawable;
import android.icu.text.BreakIterator; import android.icu.text.BreakIterator;
import android.media.MediaMetadataRetriever; import android.media.MediaMetadataRetriever;
import android.net.Uri; import android.net.Uri;
import android.opengl.Visibility;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.provider.MediaStore;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.text.Editable; import android.text.Editable;
import android.text.InputFilter; import android.text.InputFilter;
@@ -45,7 +40,6 @@ import android.text.TextWatcher;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.util.Log; import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@@ -70,7 +64,6 @@ import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import com.github.bottomSoftwareFoundation.bottom.Bottom;
import com.twitter.twittertext.TwitterTextEmojiRegex; import com.twitter.twittertext.TwitterTextEmojiRegex;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
@@ -112,7 +105,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan; import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.utils.TransferSpeedTracker; import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ComposeEditText; import org.joinmastodon.android.ui.views.ComposeEditText;
import org.joinmastodon.android.ui.views.ComposeMediaLayout; import org.joinmastodon.android.ui.views.ComposeMediaLayout;
@@ -120,7 +113,6 @@ import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout; import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout; import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import org.joinmastodon.android.utils.MastodonLanguage; import org.joinmastodon.android.utils.MastodonLanguage;
import org.joinmastodon.android.utils.StatusTextEncoder;
import org.parceler.Parcel; import org.parceler.Parcel;
import org.parceler.Parcels; import org.parceler.Parcels;
@@ -157,21 +149,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private static final int IMAGE_DESCRIPTION_RESULT=363; private static final int IMAGE_DESCRIPTION_RESULT=363;
private static final int SCHEDULED_STATUS_OPENED_RESULT=161; private static final int SCHEDULED_STATUS_OPENED_RESULT=161;
private static final int MAX_ATTACHMENTS=4; private static final int MAX_ATTACHMENTS=4;
private static final String GLITCH_LOCAL_ONLY_SUFFIX = "👁";
private static final Pattern GLITCH_LOCAL_ONLY_PATTERN = Pattern.compile("[\\s\\S]*" + GLITCH_LOCAL_ONLY_SUFFIX + "[\uFE00-\uFE0F]*");
private static final String TAG="ComposeFragment"; private static final String TAG="ComposeFragment";
public static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
// from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift // from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift
public static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))"); private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
public static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))"); private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
@SuppressLint("NewApi") // this class actually exists on 6.0 @SuppressLint("NewApi") // this class actually exists on 6.0
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance(); private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
private SizeListenerLinearLayout contentView; private SizeListenerLinearLayout contentView;
private TextView selfName, selfUsername, selfExtraText, extraText; private TextView selfName, selfUsername;
private ImageView selfAvatar; private ImageView selfAvatar;
private Account self; private Account self;
private String instanceDomain; private String instanceDomain;
@@ -207,7 +197,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private List<EmojiCategory> customEmojis; private List<EmojiCategory> customEmojis;
private CustomEmojiPopupKeyboard emojiKeyboard; private CustomEmojiPopupKeyboard emojiKeyboard;
private Status replyTo; private Status replyTo;
private Status quote;
private String initialText; private String initialText;
private String uuid; private String uuid;
private int pollDuration=24*3600; private int pollDuration=24*3600;
@@ -221,7 +210,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private View sendingOverlay; private View sendingOverlay;
private WindowManager wm; private WindowManager wm;
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC; private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
private boolean localOnly;
private ComposeAutocompleteSpan currentAutocompleteSpan; private ComposeAutocompleteSpan currentAutocompleteSpan;
private FrameLayout mainEditTextWrap; private FrameLayout mainEditTextWrap;
private ComposeAutocompleteViewController autocompleteViewController; private ComposeAutocompleteViewController autocompleteViewController;
@@ -236,7 +224,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private boolean ignoreSelectionChanges=false; private boolean ignoreSelectionChanges=false;
private Runnable updateUploadEtaRunnable; private Runnable updateUploadEtaRunnable;
private String language, encoding; private String language;
private MastodonLanguage.LanguageResolver languageResolver; private MastodonLanguage.LanguageResolver languageResolver;
private int navigationBarColorBefore; private int navigationBarColorBefore;
@@ -256,12 +244,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain); instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
languageResolver=new MastodonLanguage.LanguageResolver(instance); languageResolver=new MastodonLanguage.LanguageResolver(instance);
redraftStatus=getArguments().getBoolean("redraftStatus", false); redraftStatus=getArguments().getBoolean("redraftStatus", false);
if(getArguments().containsKey("editStatus")) if(getArguments().containsKey("editStatus")){
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus")); editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
if(getArguments().containsKey("replyTo")) }
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
if(getArguments().containsKey("quote"))
quote=Parcels.unwrap(getArguments().getParcelable("quote"));
if(instance==null){ if(instance==null){
Nav.finish(this); Nav.finish(this);
return; return;
@@ -322,20 +307,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
draftsBtn=view.findViewById(R.id.drafts_btn); draftsBtn=view.findViewById(R.id.drafts_btn);
draftsBtn.setVisibility(View.VISIBLE); draftsBtn.setVisibility(View.VISIBLE);
} else {
charCounter=view.findViewById(R.id.char_counter);
charCounter.setVisibility(View.VISIBLE);
charCounter.setText(String.valueOf(charLimit));
} }
mainEditText=view.findViewById(R.id.toot_text); mainEditText=view.findViewById(R.id.toot_text);
mainEditTextWrap=view.findViewById(R.id.toot_text_wrap); mainEditTextWrap=view.findViewById(R.id.toot_text_wrap);
charCounter=view.findViewById(R.id.char_counter);
charCounter.setText(String.valueOf(charLimit));
scrollView=view.findViewById(R.id.scroll_view); scrollView=view.findViewById(R.id.scroll_view);
selfName=view.findViewById(R.id.self_name); selfName=view.findViewById(R.id.self_name);
selfUsername=view.findViewById(R.id.self_username); selfUsername=view.findViewById(R.id.self_username);
selfAvatar=view.findViewById(R.id.self_avatar); selfAvatar=view.findViewById(R.id.self_avatar);
selfExtraText=view.findViewById(R.id.self_extra_text);
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis); HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
selfUsername.setText('@'+self.username+'@'+instanceDomain); selfUsername.setText('@'+self.username+'@'+instanceDomain);
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar)); ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
@@ -359,30 +341,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn); scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn);
sensitiveIcon=view.findViewById(R.id.sensitive_icon); sensitiveIcon=view.findViewById(R.id.sensitive_icon);
sensitiveItem=view.findViewById(R.id.sensitive_item); sensitiveItem=view.findViewById(R.id.sensitive_item);
replyText=view.findViewById(GlobalUserPreferences.replyLineAboveHeader ? R.id.reply_text : R.id.reply_text_below); replyText=view.findViewById(R.id.reply_text);
view.findViewById(GlobalUserPreferences.replyLineAboveHeader ? R.id.reply_text_below : R.id.reply_text)
.setVisibility(View.GONE);
if (isPhotoPickerAvailable()) { mediaBtn.setOnClickListener(v->openFilePicker());
PopupMenu attachPopup = new PopupMenu(getContext(), mediaBtn);
attachPopup.inflate(R.menu.attach);
attachPopup.setOnMenuItemClickListener(i -> {
openFilePicker(i.getItemId() == R.id.media);
return true;
});
UiUtils.enablePopupMenuIcons(getContext(), attachPopup);
mediaBtn.setOnClickListener(v->attachPopup.show());
mediaBtn.setOnTouchListener(attachPopup.getDragToOpenListener());
} else {
mediaBtn.setOnClickListener(v -> openFilePicker(false));
}
pollBtn.setOnClickListener(v->togglePoll()); pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText)); emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler()); spoilerBtn.setOnClickListener(v->toggleSpoiler());
localOnly = savedInstanceState != null ? savedInstanceState.getBoolean("localOnly") :
editingStatus != null ? editingStatus.localOnly : replyTo != null && replyTo.localOnly;
buildVisibilityPopup(visibilityBtn); buildVisibilityPopup(visibilityBtn);
visibilityBtn.setOnClickListener(v->visibilityPopup.show()); visibilityBtn.setOnClickListener(v->visibilityPopup.show());
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener()); visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
@@ -457,7 +421,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable()); spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable());
spoilerEdit.setBackground(spoilerBg); spoilerEdit.setBackground(spoilerBg);
if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){ if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){
hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE); spoilerEdit.setVisibility(View.VISIBLE);
spoilerBtn.setSelected(true); spoilerBtn.setSelected(true);
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){ }else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
@@ -467,13 +430,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBtn.setSelected(true); spoilerBtn.setSelected(true);
} }
sensitive = savedInstanceState==null && editingStatus != null ? editingStatus.sensitive
: savedInstanceState!=null && savedInstanceState.getBoolean("sensitive", false);
if (sensitive) {
sensitiveItem.setVisibility(View.VISIBLE);
sensitiveIcon.setSelected(true);
}
if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){ if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments"); ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments");
for(Parcelable a:serializedAttachments){ for(Parcelable a:serializedAttachments){
@@ -501,9 +457,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case UNLISTED -> R.id.vis_unlisted; case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers; case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private; case DIRECT -> R.id.vis_private;
case LOCAL -> R.id.vis_local;
}).setChecked(true); }).setChecked(true);
visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID); autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected); autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
@@ -530,7 +484,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putBoolean("pollAllowMultiple", pollAllowMultipleItem.isSelected()); outState.putBoolean("pollAllowMultiple", pollAllowMultipleItem.isSelected());
} }
outState.putBoolean("sensitive", sensitive); outState.putBoolean("sensitive", sensitive);
outState.putBoolean("localOnly", localOnly);
outState.putBoolean("hasSpoiler", hasSpoiler); outState.putBoolean("hasSpoiler", hasSpoiler);
outState.putString("language", language); outState.putString("language", language);
if(!attachments.isEmpty()){ if(!attachments.isEmpty()){
@@ -562,8 +515,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mainEditText.setSelectionListener(this); mainEditText.setSelectionListener(this);
mainEditText.addTextChangedListener(new TextWatcher(){ mainEditText.addTextChangedListener(new TextWatcher(){
private int lastChangeStart, lastChangeCount;
@Override @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){ public void beforeTextChanged(CharSequence s, int start, int count, int after){
@@ -573,16 +524,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onTextChanged(CharSequence s, int start, int before, int count){ public void onTextChanged(CharSequence s, int start, int before, int count){
if(s.length()==0) if(s.length()==0)
return; return;
lastChangeStart=start;
lastChangeCount=count;
}
@Override
public void afterTextChanged(Editable s){
if(s.length()==0)
return;
int start=lastChangeStart;
int count=lastChangeCount;
// offset one char back to catch an already typed '@' or '#' or ':' // offset one char back to catch an already typed '@' or '#' or ':'
int realStart=start; int realStart=start;
start=Math.max(0, start-1); start=Math.max(0, start-1);
@@ -628,13 +569,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
editable.removeSpan(span); editable.removeSpan(span);
} }
} }
}
@Override
public void afterTextChanged(Editable s){
updateCharCounter(); updateCharCounter();
} }
}); });
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter())); spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
if(replyTo!=null || quote!=null){ if(replyTo!=null){
Status status = quote!=null ? quote : replyTo;
View replyWrap = view.findViewById(R.id.reply_wrap); View replyWrap = view.findViewById(R.id.reply_wrap);
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { scrollView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
int scrollHeight = scrollView.getHeight(); int scrollHeight = scrollView.getHeight();
@@ -645,28 +588,27 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
scrollView.post(() -> { scrollView.post(() -> {
int bottom = scrollView.getChildAt(0).getBottom(); int bottom = scrollView.getChildAt(0).getBottom();
int delta = bottom - (scrollView.getScrollY() + scrollView.getHeight()); int delta = bottom - (scrollView.getScrollY() + scrollView.getHeight());
int space = GlobalUserPreferences.reduceMotion ? 0 : Math.min(V.dp(70), delta); int space = GlobalUserPreferences.reduceMotion ? 0 : Math.min(V.dp(120), delta);
scrollView.scrollBy(0, delta - space); scrollView.scrollBy(0, delta - space);
if (!GlobalUserPreferences.reduceMotion) { if (!GlobalUserPreferences.reduceMotion) {
scrollView.postDelayed(() -> scrollView.smoothScrollBy(0, space), 130); scrollView.postDelayed(() -> scrollView.smoothScrollBy(0, space), 100);
} }
}); });
} }
} }
}); });
View originalPost = view.findViewById(R.id.original_post); View originalPost = view.findViewById(R.id.original_post);
extraText = view.findViewById(R.id.extra_text);
originalPost.setVisibility(View.VISIBLE); originalPost.setVisibility(View.VISIBLE);
originalPost.setOnClickListener(v->{ originalPost.setOnClickListener(v->{
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status)); args.putParcelable("status", Parcels.wrap(replyTo));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0); imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ThreadFragment.class, args); Nav.go(getActivity(), ThreadFragment.class, args);
}); });
ImageView avatar = view.findViewById(R.id.avatar); ImageView avatar = view.findViewById(R.id.avatar);
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(status.account.avatar)); ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(replyTo.account.avatar));
ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){ ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
@Override @Override
public void getOutline(View view, Outline outline){ public void getOutline(View view, Outline outline){
@@ -678,62 +620,49 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
avatar.setOnClickListener(v->{ avatar.setOnClickListener(v->{
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(status.account)); args.putParcelable("profileAccount", Parcels.wrap(replyTo.account));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ProfileFragment.class, args); Nav.go(getActivity(), ProfileFragment.class, args);
}); });
((TextView) view.findViewById(R.id.name)).setText(status.account.displayName); ((TextView) view.findViewById(R.id.name)).setText(replyTo.account.displayName);
((TextView) view.findViewById(R.id.username)).setText(status.account.getDisplayUsername()); ((TextView) view.findViewById(R.id.username)).setText(replyTo.account.getDisplayUsername());
view.findViewById(R.id.visibility).setVisibility(View.GONE); view.findViewById(R.id.visibility).setVisibility(View.GONE);
Drawable visibilityIcon = getActivity().getDrawable(switch(status.visibility){ Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular; case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular; case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled; case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular; case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
case LOCAL -> R.drawable.ic_fluent_eye_20_regular;
}); });
ImageView moreBtn = view.findViewById(R.id.more); ImageView moreBtn = view.findViewById(R.id.more);
moreBtn.setImageDrawable(visibilityIcon); moreBtn.setImageDrawable(visibilityIcon);
moreBtn.setBackground(null); moreBtn.setBackground(null);
TextView timestamp = view.findViewById(R.id.timestamp); TextView timestamp = view.findViewById(R.id.timestamp);
if (status.editedAt!=null) timestamp.setText(getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(getContext(), status.editedAt))); if (replyTo.editedAt==null) timestamp.setText(UiUtils.formatRelativeTimestamp(getContext(), replyTo.createdAt));
else if (status.createdAt!=null) timestamp.setText(UiUtils.formatRelativeTimestamp(getContext(), status.createdAt)); else timestamp.setText(getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(getContext(), replyTo.editedAt)));
else timestamp.setText(""); if (replyTo.spoilerText != null && !replyTo.spoilerText.isBlank()) {
if (status.spoilerText != null && !status.spoilerText.isBlank()) {
view.findViewById(R.id.spoiler_header).setVisibility(View.VISIBLE); view.findViewById(R.id.spoiler_header).setVisibility(View.VISIBLE);
((TextView) view.findViewById(R.id.spoiler_title_inline)).setText(status.spoilerText); ((TextView) view.findViewById(R.id.spoiler_title_inline)).setText(replyTo.spoilerText);
} }
SpannableStringBuilder content = HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, accountID); SpannableStringBuilder content = HtmlParser.parse(replyTo.content, replyTo.emojis, replyTo.mentions, replyTo.tags, accountID);
LinkedTextView text = view.findViewById(R.id.text); LinkedTextView text = view.findViewById(R.id.text);
if (content.length() > 0) text.setText(content); if (content.length() > 0) text.setText(content);
else view.findViewById(R.id.display_item_text).setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16))); else view.findViewById(R.id.display_item_text).setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
replyText.setText(getString(quote!=null? R.string.sk_quoting_user : R.string.in_reply_to, status.account.displayName)); replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
int visibilityNameRes = switch (status.visibility) { int visibilityNameRes = switch (replyTo.visibility) {
case PUBLIC -> R.string.visibility_public; case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted; case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only; case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private; case DIRECT -> R.string.visibility_private;
case LOCAL -> R.string.sk_local_only;
}; };
replyText.setContentDescription(getString(R.string.in_reply_to, status.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes)); replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
replyText.setOnClickListener(v->{
scrollView.smoothScrollTo(0, 0);
});
replyText.setOnClickListener(v->{
scrollView.smoothScrollTo(0, 0);
});
ArrayList<String> mentions=new ArrayList<>(); ArrayList<String> mentions=new ArrayList<>();
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id; String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!status.account.id.equals(ownID)) if(!replyTo.account.id.equals(ownID))
mentions.add('@'+status.account.acct); mentions.add('@'+replyTo.account.acct);
if(status.rebloggedBy != null && GlobalUserPreferences.mentionRebloggerAutomatically) for(Mention mention:replyTo.mentions){
mentions.add('@'+status.rebloggedBy.acct);
for(Mention mention:status.mentions){
if(mention.id.equals(ownID)) if(mention.id.equals(ownID))
continue; continue;
String m='@'+mention.acct; String m='@'+mention.acct;
@@ -746,17 +675,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
ignoreSelectionChanges=true; ignoreSelectionChanges=true;
mainEditText.setSelection(mainEditText.length()); mainEditText.setSelection(mainEditText.length());
ignoreSelectionChanges=false; ignoreSelectionChanges=false;
if(!TextUtils.isEmpty(status.spoilerText)){ if(!TextUtils.isEmpty(replyTo.spoilerText)){
hasSpoiler=true; hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE); spoilerEdit.setVisibility(View.VISIBLE);
if(GlobalUserPreferences.prefixRepliesWithRe && !status.spoilerText.startsWith("re: ")){ spoilerEdit.setText(replyTo.spoilerText);
spoilerEdit.setText("re: " + status.spoilerText);
}else{
spoilerEdit.setText(status.spoilerText);
}
spoilerBtn.setSelected(true); spoilerBtn.setSelected(true);
} }
if (status.language != null && !status.language.isEmpty()) updateLanguage(status.language); if (replyTo.language != null && !replyTo.language.isEmpty()) updateLanguage(replyTo.language);
} }
}else if (editingStatus==null || editingStatus.inReplyToId==null){ }else if (editingStatus==null || editingStatus.inReplyToId==null){
// TODO: remove workaround after https://github.com/mastodon/mastodon-android/issues/341 gets fixed // TODO: remove workaround after https://github.com/mastodon/mastodon-android/issues/341 gets fixed
@@ -776,7 +701,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
DraftMediaAttachment da=new DraftMediaAttachment(); DraftMediaAttachment da=new DraftMediaAttachment();
da.serverAttachment=att; da.serverAttachment=att;
da.description=att.description; da.description=att.description;
da.uri=att.previewUrl!=null ? Uri.parse(att.previewUrl) : null; da.uri=Uri.parse(att.previewUrl);
da.state=AttachmentUploadState.DONE; da.state=AttachmentUploadState.DONE;
attachmentsView.addView(createMediaAttachmentView(da)); attachmentsView.addView(createMediaAttachmentView(da));
attachments.add(da); attachments.add(da);
@@ -807,7 +732,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
updateSensitive(); updateSensitive();
updateHeaders();
if(editingStatus!=null){ if(editingStatus!=null){
updateCharCounter(); updateCharCounter();
@@ -830,10 +754,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
draftsBtn = wrap.findViewById(R.id.drafts_btn); draftsBtn = wrap.findViewById(R.id.drafts_btn);
draftsBtn.setVisibility(View.VISIBLE); draftsBtn.setVisibility(View.VISIBLE);
}else{
charCounter = wrap.findViewById(R.id.char_counter);
charCounter.setVisibility(View.VISIBLE);
charCounter.setText(String.valueOf(charLimit));
} }
// draftsBtn = wrap.findViewById(R.id.drafts_btn); // draftsBtn = wrap.findViewById(R.id.drafts_btn);
@@ -862,11 +782,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener()); draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null); updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton); buildLanguageSelector(languageButton);
if (editingStatus != null && scheduledStatus == null) {
// editing an already published post
draftsBtn.setVisibility(View.GONE);
}
} }
private void navigateToUnsentPosts() { private void navigateToUnsentPosts() {
@@ -890,13 +805,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
private void updateLanguage(MastodonLanguage loc) { private void updateLanguage(MastodonLanguage loc) {
updateLanguage(loc.getLanguage(), loc.getLanguageName(), loc.getDefaultName()); language = loc.getLanguage();
} languageButton.setText(loc.getLanguageName());
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, loc.getDefaultName()));
private void updateLanguage(String languageTag, String languageName, String defaultName) {
language = languageTag;
languageButton.setText(languageName);
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, defaultName));
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@@ -906,20 +817,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
btn.setOnClickListener(v->languagePopup.show()); btn.setOnClickListener(v->languagePopup.show());
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences; Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
if (language != null) updateLanguage(language); updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
else updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
? languageResolver.from(prefs.postingDefaultLanguage) ? languageResolver.from(prefs.postingDefaultLanguage)
: languageResolver.getDefault()); : languageResolver.getDefault());
Menu languageMenu = languagePopup.getMenu(); Menu languageMenu = languagePopup.getMenu();
for (String recentLanguage : Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)) { for (String recentLanguage : Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)) {
if (recentLanguage.equals("bottom")) {
addBottomLanguage(languageMenu);
} else {
MastodonLanguage l = languageResolver.from(recentLanguage); MastodonLanguage l = languageResolver.from(recentLanguage);
languageMenu.add(0, allLanguages.indexOf(l), Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName())); languageMenu.add(0, allLanguages.indexOf(l), Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
} }
}
SubMenu allLanguagesMenu = languageMenu.addSubMenu(R.string.sk_available_languages); SubMenu allLanguagesMenu = languageMenu.addSubMenu(R.string.sk_available_languages);
for (int i = 0; i < allLanguages.size(); i++) { for (int i = 0; i < allLanguages.size(); i++) {
@@ -927,33 +833,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
allLanguagesMenu.add(0, i, Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName())); allLanguagesMenu.add(0, i, Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
} }
if (GlobalUserPreferences.bottomEncoding) addBottomLanguage(allLanguagesMenu);
btn.setOnLongClickListener(v->{
btn.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (!GlobalUserPreferences.bottomEncoding) addBottomLanguage(allLanguagesMenu);
return false;
});
languagePopup.setOnMenuItemClickListener(i->{ languagePopup.setOnMenuItemClickListener(i->{
if (i.hasSubMenu()) return false; if (i.hasSubMenu()) return false;
if (i.getItemId() == allLanguages.size()) {
updateLanguage(language, "\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48", "bottom");
encoding = "bottom";
} else {
updateLanguage(allLanguages.get(i.getItemId())); updateLanguage(allLanguages.get(i.getItemId()));
encoding = null;
}
return true; return true;
}); });
} }
private void addBottomLanguage(Menu menu) {
if (menu.findItem(allLanguages.size()) == null) {
menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)");
}
}
@Override @Override
public boolean onOptionsItemSelected(MenuItem item){ public boolean onOptionsItemSelected(MenuItem item){
return true; return true;
@@ -983,9 +869,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(hasSpoiler){ if(hasSpoiler){
charCount+=spoilerEdit.length(); charCount+=spoilerEdit.length();
} }
if (localOnly && GlobalUserPreferences.accountsInGlitchMode.contains(accountID)) {
charCount -= GLITCH_LOCAL_ONLY_SUFFIX.length();
}
charCounter.setText(String.valueOf(charLimit-charCount)); charCounter.setText(String.valueOf(charLimit-charCount));
trimmedCharCount=text.toString().trim().length(); trimmedCharCount=text.toString().trim().length();
updatePublishButtonState(); updatePublishButtonState();
@@ -1018,15 +901,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
nonDoneAttachmentCount++; nonDoneAttachmentCount++;
} }
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1)); publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
// sendError.setVisibility(View.GONE); sendError.setVisibility(View.GONE);
} }
private void onCustomEmojiClick(Emoji emoji){ private void onCustomEmojiClick(Emoji emoji){
if(getActivity().getCurrentFocus() instanceof EditText edit){ int start=mainEditText.getSelectionStart();
int start=edit.getSelectionStart(); String prefix=start>0 && !Character.isWhitespace(mainEditText.getText().charAt(start-1)) ? " :" : ":";
String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":"; mainEditText.getText().replace(start, mainEditText.getSelectionEnd(), prefix+emoji.shortcode+':');
edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':');
}
} }
@Override @Override
@@ -1040,8 +921,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
&& statusVisibility != StatusPrivacy.DIRECT && statusVisibility != StatusPrivacy.DIRECT
&& !attachments.stream().allMatch(attachment -> attachment.description != null && !attachment.description.isBlank())) { && !attachments.stream().allMatch(attachment -> attachment.description != null && !attachment.description.isBlank())) {
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.mo_no_image_desc_title) .setTitle(R.string.sk_no_image_desc_title)
.setMessage(R.string.mo_no_image_desc) .setMessage(R.string.sk_no_image_desc)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.publish, (dialog, i)-> publish()) .setPositiveButton(R.string.publish, (dialog, i)-> publish())
.show(); .show();
@@ -1088,53 +969,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
private void publish(){ private void publish(){
publish(false);
}
private void publish(boolean force){
String text=mainEditText.getText().toString(); String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request(); CreateStatus.Request req=new CreateStatus.Request();
if ("bottom".equals(encoding)) {
text = new StatusTextEncoder(Bottom::encode).encode(text);
req.spoilerText = "bottom-encoded emoji spam";
}
if (localOnly &&
GlobalUserPreferences.accountsInGlitchMode.contains(accountID) &&
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) {
text += " " + GLITCH_LOCAL_ONLY_SUFFIX;
}
req.status=text; req.status=text;
req.localOnly=localOnly; req.visibility=statusVisibility;
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive; req.sensitive=sensitive;
req.language=language; req.language=language;
req.scheduledAt = scheduledAt; req.scheduledAt = scheduledAt;
if(!attachments.isEmpty()){ if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList()); req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
Optional<DraftMediaAttachment> withoutAltText = attachments.stream().filter(a -> a.description == null || a.description.isBlank()).findFirst();
boolean isDraft = scheduledAt != null && scheduledAt.isAfter(DRAFTS_AFTER_INSTANT);
if (!force && !GlobalUserPreferences.disableAltTextReminder && !isDraft && withoutAltText.isPresent()) {
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_alt_text_missing_title)
.setMessage(R.string.sk_alt_text_missing)
.setPositiveButton(R.string.add_alt_text, (d, w) -> editMediaDescription(withoutAltText.get()))
.setNegativeButton(R.string.sk_publish_anyway, (d, w) -> publish(true))
.show();
return;
}
}
// 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) -> publish(true))
.setNegativeButton(R.string.publish, (d, w) -> {
updateScheduledAt(null);
publish();
})
.show();
return;
} }
if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){ if(replyTo!=null || (editingStatus != null && editingStatus.inReplyToId!=null)){
req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id; req.inReplyToId=editingStatus!=null ? editingStatus.inReplyToId : replyTo.id;
@@ -1149,9 +992,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(hasSpoiler && spoilerEdit.length()>0){ if(hasSpoiler && spoilerEdit.length()>0){
req.spoilerText=spoilerEdit.getText().toString(); req.spoilerText=spoilerEdit.getText().toString();
} }
if(quote != null){
req.quoteId=quote.id;
}
if(uuid==null) if(uuid==null)
uuid=UUID.randomUUID().toString(); uuid=UUID.randomUUID().toString();
@@ -1184,9 +1024,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}else{ }else{
E.post(new StatusUpdatedEvent(result)); E.post(new StatusUpdatedEvent(result));
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
Nav.finish(ComposeFragment.this); Nav.finish(ComposeFragment.this);
}
if (getArguments().getBoolean("navigateToStatus", false)) { if (getArguments().getBoolean("navigateToStatus", false)) {
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
@@ -1245,14 +1083,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
List<String> newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)); List<String> newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages));
newRecentLanguages.remove(language); newRecentLanguages.remove(language);
newRecentLanguages.add(0, language); newRecentLanguages.add(0, language);
if (encoding != null) {
newRecentLanguages.remove(encoding);
newRecentLanguages.add(0, encoding);
}
if ("bottom".equals(encoding) && !GlobalUserPreferences.bottomEncoding) {
GlobalUserPreferences.bottomEncoding = true;
GlobalUserPreferences.save();
}
recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList())); recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList()));
GlobalUserPreferences.save(); GlobalUserPreferences.save();
} }
@@ -1318,14 +1148,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
private void confirmDiscardDraftAndFinish(){ private void confirmDiscardDraftAndFinish(){
boolean attachmentsPending = attachments.stream().anyMatch(att -> att.state != AttachmentUploadState.DONE); new M3AlertDialogBuilder(getActivity())
if (attachmentsPending) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_unfinished_attachments)
.setMessage(R.string.sk_unfinished_attachments_message)
.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) .setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
.setPositiveButton(R.string.save, (d, w) -> { .setPositiveButton(R.string.save, (d, w) -> {
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt); updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
@@ -1335,39 +1158,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
.show(); .show();
} }
private void openFilePicker(){
/** Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
* Builds the correct intent for the device version to select media.
*
* <p>For Device version > T or R_SDK_v2, use the android platform photopicker via
* {@link MediaStore#ACTION_PICK_IMAGES}
*
* <p>For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
*/
private void openFilePicker(boolean photoPicker){
Intent intent;
boolean usePhotoPicker=photoPicker && isPhotoPickerAvailable();
if(usePhotoPicker){
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MAX_ATTACHMENTS-getMediaAttachmentsCount());
}else{
intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*"); intent.setType("*/*");
} if(instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
if(!usePhotoPicker && instance.configuration!=null && intent.putExtra(Intent.EXTRA_MIME_TYPES, instance.configuration.mediaAttachments.supportedMimeTypes.toArray(new String[0]));
instance.configuration.mediaAttachments!=null &&
instance.configuration.mediaAttachments.supportedMimeTypes!=null &&
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
intent.putExtra(Intent.EXTRA_MIME_TYPES,
instance.configuration.mediaAttachments.supportedMimeTypes.toArray(
new String[0]));
}else{ }else{
if(!usePhotoPicker){
// If photo picker is being used these are the default mimetypes.
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"}); intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
} }
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(intent, MEDIA_RESULT); startActivityForResult(intent, MEDIA_RESULT);
} }
@@ -1387,7 +1186,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
} }
@SuppressLint("StringFormatInvalid")
private boolean addMediaAttachment(Uri uri, String description){ private boolean addMediaAttachment(Uri uri, String description){
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS){ if(getMediaAttachmentsCount()==MAX_ATTACHMENTS){
showMediaAttachmentError(getResources().getQuantityString(R.plurals.cant_add_more_than_x_attachments, MAX_ATTACHMENTS, MAX_ATTACHMENTS)); showMediaAttachmentError(getResources().getQuantityString(R.plurals.cant_add_more_than_x_attachments, MAX_ATTACHMENTS, MAX_ATTACHMENTS));
@@ -1450,7 +1248,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false); View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
ImageView img=thumb.findViewById(R.id.thumb); ImageView img=thumb.findViewById(R.id.thumb);
if(draft.serverAttachment!=null){ if(draft.serverAttachment!=null){
if(draft.serverAttachment.previewUrl!=null)
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250))); ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
}else{ }else{
if(draft.mimeType.startsWith("image/")){ if(draft.mimeType.startsWith("image/")){
@@ -1604,7 +1401,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(att.isUploadingOrProcessing()) if(att.isUploadingOrProcessing())
att.cancelUpload(); att.cancelUpload();
attachments.remove(att); attachments.remove(att);
if(!areThereAnyUploadingAttachments())
uploadNextQueuedAttachment(); uploadNextQueuedAttachment();
attachmentsView.removeView(att.view); attachmentsView.removeView(att.view);
if(getMediaAttachmentsCount()==0) if(getMediaAttachmentsCount()==0)
@@ -1786,20 +1582,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
menu.getMenu().add(0, 2, 0, getResources().getQuantityString(R.plurals.x_minutes, 30, 30)); menu.getMenu().add(0, 2, 0, getResources().getQuantityString(R.plurals.x_minutes, 30, 30));
menu.getMenu().add(0, 3, 0, getResources().getQuantityString(R.plurals.x_hours, 1, 1)); menu.getMenu().add(0, 3, 0, getResources().getQuantityString(R.plurals.x_hours, 1, 1));
menu.getMenu().add(0, 4, 0, getResources().getQuantityString(R.plurals.x_hours, 6, 6)); menu.getMenu().add(0, 4, 0, getResources().getQuantityString(R.plurals.x_hours, 6, 6));
menu.getMenu().add(0, 5, 0, getResources().getQuantityString(R.plurals.x_hours, 12, 12)); menu.getMenu().add(0, 5, 0, getResources().getQuantityString(R.plurals.x_days, 1, 1));
menu.getMenu().add(0, 6, 0, getResources().getQuantityString(R.plurals.x_days, 1, 1)); menu.getMenu().add(0, 6, 0, getResources().getQuantityString(R.plurals.x_days, 3, 3));
menu.getMenu().add(0, 7, 0, getResources().getQuantityString(R.plurals.x_days, 3, 3)); menu.getMenu().add(0, 7, 0, getResources().getQuantityString(R.plurals.x_days, 7, 7));
menu.getMenu().add(0, 8, 0, getResources().getQuantityString(R.plurals.x_days, 7, 7));
menu.setOnMenuItemClickListener(item->{ menu.setOnMenuItemClickListener(item->{
pollDuration=switch(item.getItemId()){ pollDuration=switch(item.getItemId()){
case 1 -> 5*60; case 1 -> 5*60;
case 2 -> 30*60; case 2 -> 30*60;
case 3 -> 3600; case 3 -> 3600;
case 4 -> 6*3600; case 4 -> 6*3600;
case 5 -> 12*3600; case 5 -> 24*3600;
case 6 -> 24*3600; case 6 -> 3*24*3600;
case 7 -> 3*24*3600; case 7 -> 7*24*3600;
case 8 -> 7*24*3600;
default -> throw new IllegalStateException("Unexpected value: "+item.getItemId()); default -> throw new IllegalStateException("Unexpected value: "+item.getItemId());
}; };
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString())); pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString()));
@@ -1908,33 +1702,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
return attachments.size(); return attachments.size();
} }
private void updateHeaders() {
UiUtils.setExtraTextInfo(getContext(), selfExtraText, statusVisibility, localOnly);
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, replyTo.visibility, replyTo.localOnly);
}
private void buildVisibilityPopup(View v){ private void buildVisibilityPopup(View v){
visibilityPopup=new PopupMenu(getActivity(), v); visibilityPopup=new PopupMenu(getActivity(), v);
visibilityPopup.inflate(R.menu.compose_visibility); visibilityPopup.inflate(R.menu.compose_visibility);
Menu m=visibilityPopup.getMenu(); Menu m=visibilityPopup.getMenu();
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.pleroma != null) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);
localOnlyItem.setChecked(localOnly);
Status status = editingStatus != null ? editingStatus : replyTo;
if (!prefsSaysSupported) {
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
if (GLITCH_LOCAL_ONLY_PATTERN.matcher(status.getStrippedText()).matches()) {
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
}
GlobalUserPreferences.save();
}
}
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup); UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) m.setGroupDividerEnabled(true); m.setGroupCheckable(0, true, true);
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
@Override @Override
public boolean onMenuItemClick(MenuItem item){ public boolean onMenuItemClick(MenuItem item){
@@ -1947,25 +1720,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility=StatusPrivacy.PRIVATE; statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){ }else if(id==R.id.vis_private){
statusVisibility=StatusPrivacy.DIRECT; statusVisibility=StatusPrivacy.DIRECT;
}else if(id==R.id.vis_local){
statusVisibility=StatusPrivacy.LOCAL;
} }
if (id == R.id.local_only) {
localOnly = !item.isChecked();
item.setChecked(localOnly);
} else {
item.setChecked(true); item.setChecked(true);
}
updateVisibilityIcon(); updateVisibilityIcon();
updateHeaders();
return true; return true;
} }
}); });
} }
private void loadDefaultStatusVisibility(Bundle savedInstanceState) { private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
if(replyTo != null) { if(getArguments().containsKey("replyTo")){
statusVisibility = (replyTo.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : replyTo.visibility); replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
statusVisibility = replyTo.visibility;
} }
// A saved privacy setting from a previous compose session wins over the reply visibility // A saved privacy setting from a previous compose session wins over the reply visibility
@@ -1973,15 +1739,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility"); statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
} }
AccountSessionManager asm = AccountSessionManager.getInstance(); Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
Preferences prefs = asm.getAccount(accountID).preferences;
if (prefs != null) { if (prefs != null) {
// Only override the reply visibility if our preference is more private // Only override the reply visibility if our preference is more private
// (and we're not replying to ourselves, or not at all) if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility) && statusVisibility = switch (prefs.postingDefaultVisibility) {
(replyTo == null || !asm.isSelf(accountID, replyTo.account))) { case PUBLIC -> StatusPrivacy.PUBLIC;
statusVisibility = prefs.postingDefaultVisibility; case UNLISTED -> StatusPrivacy.UNLISTED;
} case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
} }
// A saved privacy setting from a previous compose session wins over all // A saved privacy setting from a previous compose session wins over all
@@ -1989,6 +1756,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility"); statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
} }
} }
}
private void updateVisibilityIcon(){ private void updateVisibilityIcon(){
if(statusVisibility==null){ // TODO find out why this happens if(statusVisibility==null){ // TODO find out why this happens
@@ -1996,10 +1764,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
visibilityBtn.setImageResource(switch(statusVisibility){ visibilityBtn.setImageResource(switch(statusVisibility){
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular; case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
case UNLISTED -> R.drawable.ic_fluent_lock_open_24_regular; case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
case PRIVATE -> R.drawable.ic_fluent_lock_closed_24_filled; case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
case DIRECT -> R.drawable.ic_fluent_mention_24_regular; case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
case LOCAL -> R.drawable.ic_fluent_eye_24_regular;
}); });
} }
@@ -2121,14 +1888,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
}); });
} }
private void editMediaDescription(DraftMediaAttachment att) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("attachment", att.serverAttachment.id);
args.putParcelable("uri", att.uri);
args.putString("existingDescription", att.description);
Nav.goForResult(getActivity(), ComposeImageDescriptionFragment.class, args, IMAGE_DESCRIPTION_RESULT, this);
}
@Override @Override
public CharSequence getTitle(){ public CharSequence getTitle(){

View File

@@ -1,74 +0,0 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import org.joinmastodon.android.DomainManager;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class CustomLocalTimelineFragment extends StatusListFragment {
// private String name;
private String domain;
private String maxID;
@Override
protected boolean wantsComposeButton() {
return false;
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
domain=getArguments().getString("domain");
updateTitle(domain);
setHasOptionsMenu(true);
}
private void updateTitle(String domain) {
this.domain = domain;
setTitle(this.domain);
}
@Override
public String getDomain() {
return domain;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
result.stream().forEach(status -> {
status.account.acct += "@"+domain;
status.mentions.forEach(mention -> mention.id = null);
status.reloadWhenClicked = true;
});
onDataLoaded(result, !result.isEmpty());
}
})
.execNoAuth(domain);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
}

View File

@@ -1,15 +0,0 @@
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 "";
}
}

View File

@@ -1,388 +0,0 @@
package org.joinmastodon.android.fragments;
import static android.view.Menu.NONE;
import static org.joinmastodon.android.ui.utils.UiUtils.makeBackItem;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.InputType;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.model.CustomLocalTimeline;
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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class EditTimelinesFragment extends RecyclerFragment<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 final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
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");
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);
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
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.colorPollVoted, 0.5f, 56, 16));
}
@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) {
addNewLocalTimeline();
return true;
}
TimelineDefinition tl = timelineByMenuItem.get(item);
if (tl != null) {
data.add(tl.copy());
adapter.notifyItemInserted(data.size());
saveTimelines();
updateOptionsMenu();
};
return true;
}
private void addNewLocalTimeline() {
FrameLayout inputWrap = new FrameLayout(getContext());
EditText input = new EditText(getContext());
input.setHint(R.string.sk_example_domain);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(V.dp(16), V.dp(4), V.dp(16), V.dp(16));
input.setLayoutParams(params);
inputWrap.addView(input);
new M3AlertDialogBuilder(getContext()).setTitle(R.string.mo_add_custom_server_local_timeline).setView(inputWrap)
.setPositiveButton(R.string.save, (d, which) -> {
TimelineDefinition tl = TimelineDefinition.ofCustomLocalTimeline(input.getText().toString().trim());
data.add(tl);
saveTimelines();
})
.setNegativeButton(R.string.cancel, (d, which) -> {
})
.show();
}
private void addTimelineToOptions(TimelineDefinition tl, Menu menu) {
if (data.contains(tl)) return;
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, tl.getTitle(getContext()));
item.setIcon(tl.getIcon().iconRes);
timelineByMenuItem.put(item, tl);
}
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 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);
addLocalTimelines.setIcon(R.drawable.ic_fluent_add_24_regular);
makeBackItem(timelinesMenu);
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
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);
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
}
private void saveTimelines() {
updated = true;
GlobalUserPreferences.pinnedTimelines.put(accountID, data.size() > 0 ? data : List.of(TimelineDefinition.HOME_TIMELINE));
GlobalUserPreferences.save();
}
private void removeTimeline(int position) {
data.remove(position);
adapter.notifyItemRemoved(position);
saveTimelines();
updateOptionsMenu();
}
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
updateOptionsMenu();
}
@Override
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
return adapter = new TimelinesAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
}
@Override
public void onDestroy() {
super.onDestroy();
if (updated) UiUtils.restartApp();
}
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 int getItemCount() {
return data.size();
}
}
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);
}
@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 onClick() {
Context ctx = getContext();
LinearLayout view = (LinearLayout) getActivity().getLayoutInflater()
.inflate(R.layout.edit_timeline, (ViewGroup) itemView, false);
TextInputFrameLayout inputLayout = view.findViewById(R.id.input);
EditText editText = inputLayout.getEditText();
editText.setText(item.getCustomTitle());
editText.setHint(item.getDefaultTitle(ctx));
ImageButton btn = view.findViewById(R.id.button);
PopupMenu popup = new PopupMenu(ctx, btn);
TimelineDefinition.Icon currentIcon = item.getIcon();
btn.setImageResource(currentIcon.iconRes);
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
btn.setOnTouchListener(popup.getDragToOpenListener());
btn.setOnClickListener(l -> popup.show());
Menu menu = popup.getMenu();
TimelineDefinition.Icon defaultIcon = item.getDefaultIcon();
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.equals(item.getIcon())) 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.setContentDescription(ctx.getString(icon.nameRes));
item.setIcon(icon);
return true;
});
new M3AlertDialogBuilder(ctx)
.setTitle(R.string.sk_edit_timeline)
.setView(view)
.setPositiveButton(R.string.save, (d, which) -> {
item.setTitle(editText.getText().toString().trim());
rebind();
saveTimelines();
})
.setNeutralButton(R.string.sk_remove, (d, which) ->
removeTimeline(getAbsoluteAdapterPosition()))
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
btn.requestFocus();
}
}
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 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 onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAbsoluteAdapterPosition();
removeTimeline(position);
}
}
}

View File

@@ -25,7 +25,6 @@ public class FavoritedStatusListFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(HeaderPaginationList<Status> result){ public void onSuccess(HeaderPaginationList<Status> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null) if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id"); nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else else

View File

@@ -38,6 +38,7 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -46,7 +47,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
private String accountID; private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap(); private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest; private GetAccountRelationships relationshipsRequest;
@@ -79,7 +80,6 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(HeaderPaginationList<Account> result){ public void onSuccess(HeaderPaginationList<Account> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null) if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id"); nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else else
@@ -148,11 +148,6 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{ private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){ public AccountsAdapter(){

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments; package org.joinmastodon.android.fragments;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
@@ -12,14 +11,14 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags; import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop { public class FollowedHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop {
private String nextMaxID; private String nextMaxID;
private String accountId; private String accountId;
@@ -42,19 +41,12 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
loadData(); loadData();
} }
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override @Override
protected void doLoadData(int offset, int count){ protected void doLoadData(int offset, int count){
currentRequest=new GetFollowedHashtags(offset==0 ? null : nextMaxID, null, count, null) currentRequest=new GetFollowedHashtags(offset==0 ? null : nextMaxID, null, count, null)
.setCallback(new SimpleCallback<>(this){ .setCallback(new SimpleCallback<>(this){
@Override @Override
public void onSuccess(HeaderPaginationList<Hashtag> result){ public void onSuccess(HeaderPaginationList<Hashtag> result){
if (getActivity() == null) return;
if(result.nextPageUri!=null) if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id"); nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else else
@@ -75,11 +67,6 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
smoothScrollRecyclerViewToTop(list); smoothScrollRecyclerViewToTop(list);
} }
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{ private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull @NonNull
@Override @Override

View File

@@ -1,7 +0,0 @@
package org.joinmastodon.android.fragments;
import android.view.View;
public interface HasFab {
View getFab();
}

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