Compare commits

...

44 Commits

Author SHA1 Message Date
Eugen Rochko 2c47a831b0
Merge a6f1943e05 into 29ab502d2e 2024-05-14 18:09:37 +00:00
Eugen Rochko a6f1943e05 New translations strings.xml (French) 2024-05-14 11:09:34 -07:00
Eugen Rochko 91ff898ac7 New translations full_description.txt (Interlingua) 2024-05-14 08:56:24 -07:00
Eugen Rochko 5c705b0f22 New translations strings.xml (Interlingua) 2024-05-14 08:56:23 -07:00
Eugen Rochko 77a07ffbb1 New translations full_description.txt (Interlingua) 2024-05-14 07:12:55 -07:00
Eugen Rochko 559ec3ecb3 New translations strings.xml (Interlingua) 2024-05-14 07:12:54 -07:00
Eugen Rochko 17bdd54f63 New translations strings.xml (Interlingua) 2024-05-14 05:35:09 -07:00
Gregory K 29ab502d2e
Merge pull request #833 from NorbiPeti/master
Display more user-friendly error messages
2024-05-13 01:49:34 +03:00
NorbiPeti 629e65edba Update error messages and remove the unknown error text 2024-05-13 00:08:18 +02:00
Grishka 97e16b9f73 Use `image_matrix_limit` from instance configuration if available 2024-05-07 23:04:43 +03:00
Grishka bc78c61009 Fix #835 2024-05-06 12:56:56 +03:00
Grishka 4a1b1e19e8 Fix #834 2024-05-04 21:20:20 +03:00
NorbiPeti c7b8cc72fc Display more user-friendly error messages
Instead of displaying the Java
exception, this change displays a more user-friendly message for some
common network-related issues.
Fixes mastodon/mastodon-android#667
2024-05-02 19:02:03 +02:00
Grishka 0e868f0be0 English proper nouns are a mess 2024-05-01 17:39:56 +03:00
Grishka 67847d90da Fix #832 2024-05-01 17:38:22 +03:00
Gregory K b0c77d42c0
Merge pull request #829 from Endeavour233/avoid-leak-response-body
Opti: avoid leaking response body when request is cancelled
2024-04-21 18:11:02 +03:00
shimura233 bfc0076429 opti: avoid leaking response body when it's not used. 2024-04-21 20:29:15 +08:00
Grishka 50760471d5 Add context menus to post footer buttons (AND-161) 2024-04-20 07:12:51 +03:00
Grishka a6df8cb62d Pressed state for alt badge (AND-157) 2024-04-20 06:57:14 +03:00
Grishka 873711939d Post header tap area thing (AND-160) 2024-04-20 06:51:30 +03:00
Grishka 2bd13eb3ba Label unlabeled things for accessibility 2024-04-20 06:34:21 +03:00
Grishka d423f17342 Animate privacy button changes (AND-158) 2024-04-20 06:27:47 +03:00
Grishka 09be5b3f97 Add descriptions to post visibility options 2024-04-20 04:48:57 +03:00
Grishka 1124bc48c2 Update CI Ruby to 3.3.0 and add Gemfile.lock 2024-04-12 18:31:43 +03:00
Grishka 69b7484a4a Merge commit '4dd5a80ef27bd4abf1eaa272d1e3c67b7d9a3a13' 2024-04-12 17:59:55 +03:00
Grishka 19939e457b Prepare for release 2024-04-12 17:57:14 +03:00
Grishka 3e256d41d2 Fix alert text size 2024-04-11 22:36:55 +03:00
Grishka 72f546ed15 update string 2024-04-11 17:42:18 +03:00
Gregory K 0b48414715
Merge pull request #824 from FineFindus/fix/featured-hastag-crash
fix: include account when opening FeaturedHashtags
2024-04-10 17:25:56 +03:00
FineFindus ca67c1eaca
fix: include account when opening FeaturedHashtags
Closes https://github.com/mastodon/mastodon-android/issues/803.
2024-04-10 16:16:11 +02:00
Grishka 36f4770cae Media layout: improve the case of two horizontal attachments 2024-04-02 05:09:55 +03:00
Grishka b7251972a8 Show the profile header view if we know the username 2024-03-28 23:00:03 +03:00
Grishka a2dec4f7cf Notification requests tweaks 2024-03-22 00:49:42 +03:00
Grishka 441567f9d2 Notification requests (AND-154) 2024-03-20 23:18:04 +03:00
Grishka f888091e22 Add unlisted visibility option as "quiet public"
closes #189, closes #103, closes #37
2024-03-18 20:34:23 +03:00
Grishka e59cf2afca Make alt text selectable 2024-03-18 20:25:28 +03:00
Grishka 5622c93bd9 Fix fragment stack breaking after opening a notification 2024-03-17 04:24:12 +03:00
Grishka bf55b5a802 Swap poll options around 2024-03-13 17:13:25 +03:00
Grishka 49bf04c6c6 Tweak line height for posts
#791
2024-03-13 17:05:57 +03:00
Grishka 5be6faa07c New posts button tweaks 2024-03-11 18:09:29 +03:00
Grishka ff7948ad83 Add conversation muting 2024-03-11 13:31:32 +03:00
Grishka 3972ab207c New post notifications (AND-151) 2024-03-11 13:18:45 +03:00
Gregory K 33a8f1dab4
Merge pull request #795 from Arthur-GYT/per-app-language
Enable auto generated per-app language file
2024-03-11 12:06:02 +03:00
Arthur-GYT 47aa7fc191 Enable auto generated per-app language file 2024-03-09 19:07:04 +01:00
92 changed files with 2007 additions and 330 deletions

View File

@ -21,7 +21,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.2
ruby-version: 3.3.0
bundler-cache: true
- name: Set up Android SDK

218
Gemfile.lock Normal file
View File

@ -0,0 +1,218 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.913.0)
aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.78.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.110.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
base64
mini_magick (4.12.0)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.4.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.0.5)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.6)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.5.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-22
ruby
DEPENDENCIES
fastlane
BUNDLED WITH
2.5.4

View File

@ -0,0 +1,8 @@
- Adjust filters in the Notifications tab to silence unwanted alerts*
- Opt into push notifications when a user posts by tapping the bell 🔔 in the top corner of a user's profile.
- Mute overly active conversation notifications via the More button ⋮ on your posts
- When writing a post, choose Quiet public 🌙 visibility to avoid appearing in feeds or searches
- Improved post legibility with adjusted line height
- Bug fixes
*Your server must support filtered notifications to see this option.

View File

@ -1,16 +1,16 @@
Mastodon es le plus grande rete social decentralisate sur internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Mastodon es le plus grande rete social decentralisate sur internet. In vice que un sol sito web, illo es un rete de milliones de usatores in communitates independente que pote tote interager le un con le altere, perfectemente. Non importa que te interessa, tu pote incontrar personas passionate que posta re illo sur Mastodon!
Junge te a un communitate e crea tu profilo. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Junge te a un communitate e crea tu profilo. Trova e seque personas fascinante e lege lor messages in un chronogramma chronologic libere de avisos publicitari. Exprime te mesme con emoticone personalisate, imagines, GIFs, videos, e audio in messages de 500-characteres. Responde a messages argumentos e promotiones de quicunque pro compartir grande substantias. Trova nove contos a sequer e hashtags de tendentia pro expander tu rete.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Mastodon es producite con attention a confidentialitate e securitate. Decide si tu messages es compartite con tu sequaces, solo le gente que tu mentiona, o tote le mundo. Le avisos re contentos te permitte de celar messages que contine material sensibile o provocatori usque tu es preste a interager con illes. Cata communitate ha su proprie lineas guida e moderatores pro mantener su membros secur. Robuste utensiles de blocada e de reporto pro adjutar a impedir abusos.
Altere functiones:
Dark Mode: Read posts in light, dark, or true black mode
Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Modo obscur: lege messages in modo clar, obscur, o francamente nigre
Modo obscur: lege messages in modo clar, obscur, o francamente nigre
• Explora: hashtags de tendentia e contos es a portata de mano
• Notificationes: recipe notificationes circa nove sequaces, responsas, e promotiones
Compartir: publica directemente sur Mastodon ab qualcunque folio de compartimento in ulle app
Pachydermo: nostre mascotte es un adorabile elephante, e tu lo videra apparer de tempore in tempore
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.
Mastodon es un organisation non lucrative registrate e le disveloppamento es supportate directemente per tu donationes. Il non ha alcun annuncios publicitari, alcun monetisation e capital de hasardo, e nos projecta mantener lo in iste condition.

View File

@ -4,15 +4,18 @@ plugins {
}
android {
androidResources {
generateLocaleConfig = true
}
compileSdk 33
defaultConfig {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 89
versionName "2.4.1"
versionCode 97
versionName "2.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "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", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "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"
}
buildTypes {
@ -87,7 +90,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.16'
implementation 'me.grishka.appkit:appkit:1.2.17'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@ -31,7 +31,6 @@
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:largeHeap="true">

View File

@ -152,6 +152,11 @@ public class MainActivity extends FragmentStackActivity{
}
fragment.setArguments(args);
showFragment(fragment);
Intent intent=getIntent();
intent.removeExtra("fromNotification");
intent.removeExtra("notification");
intent.removeExtra("accountID");
setIntent(intent);
}
private void showCompose(){

View File

@ -113,8 +113,10 @@ public class MastodonAPIController{
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
if(req.canceled)
if(req.canceled){
response.close();
return;
}
if(BuildConfig.DEBUG)
Log.d(TAG, logTag(session)+hreq+" received response: "+response);
synchronized(req){

View File

@ -5,31 +5,62 @@ import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.R;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{
public final String error;
public final int httpStatus;
public final Throwable underlyingException;
public final int messageResource;
public MastodonErrorResponse(String error, int httpStatus, Throwable exception){
this.error=error;
this.httpStatus=httpStatus;
this.underlyingException=exception;
if(exception instanceof UnknownHostException){
this.messageResource=R.string.could_not_reach_server;
}else if(exception instanceof SocketTimeoutException){
this.messageResource=R.string.connection_timed_out;
}else if(exception instanceof JsonSyntaxException || exception instanceof JsonIOException || httpStatus>=500){
this.messageResource=R.string.server_error;
}else if(httpStatus==404){
this.messageResource=R.string.not_found;
}else{
this.messageResource=0;
}
}
@Override
public void bindErrorView(View view){
TextView text=view.findViewById(R.id.error_text);
text.setText(error);
String message;
if(messageResource>0){
message=view.getContext().getString(messageResource, error);
}else{
message=error;
}
text.setText(message);
}
@Override
public void showToast(Context context){
if(context==null)
return;
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
String message;
if(messageResource>0){
message=context.getString(messageResource, error);
}else{
message=error;
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.annotations.SerializedName;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils;
@ -12,6 +13,10 @@ import java.util.List;
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
this(maxID, limit, includeTypes, null);
}
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
@ -25,6 +30,8 @@ public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
addQueryParameter("exclude_types[]", type);
}
}
if(!TextUtils.isEmpty(onlyAccountID))
addQueryParameter("account_id", onlyAccountID);
removeUnsupportedItems=true;
}
}

View File

@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationsPolicy;
public class GetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
public GetNotificationsPolicy(){
super(HttpMethod.GET, "/notifications/policy", NotificationsPolicy.class);
}
}

View File

@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class RespondToNotificationRequest extends ResultlessMastodonAPIRequest{
public RespondToNotificationRequest(String id, boolean allow){
super(HttpMethod.POST, "/notifications/requests/"+id+(allow ? "/accept" : "/dismiss"));
setRequestBody(new Object());
}
}

View File

@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationsPolicy;
public class SetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
public SetNotificationsPolicy(NotificationsPolicy policy){
super(HttpMethod.PUT, "/notifications/policy", NotificationsPolicy.class);
setRequestBody(policy);
}
}

View File

@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusConversationMuted extends MastodonAPIRequest<Status>{
public SetStatusConversationMuted(String id, boolean muted){
super(HttpMethod.POST, "/statuses/"+id+(muted ? "/mute" : "/unmute"), Status.class);
setRequestBody(new Object());
}
}

View File

@ -0,0 +1,10 @@
package org.joinmastodon.android.events;
public class NotificationRequestRespondedEvent{
public final String accountID, requestID;
public NotificationRequestRespondedEvent(String accountID, String requestID){
this.accountID=accountID;
this.requestID=requestID;
}
}

View File

@ -0,0 +1,181 @@
package org.joinmastodon.android.fragments;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
public class AccountNotificationsListFragment extends BaseNotificationsListFragment{
private Account account;
private String requestID;
private TextView expandedTitle;
private boolean choiceMade, allowed;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
requestID=getArguments().getString("requestID");
setTitleMarqueeEnabled(false);
loadData();
setTitle(getString(R.string.notifications_from_user, account.displayName));
setHasOptionsMenu(true);
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && endMark!=null)
endMark.setVisibility(View.GONE);
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Notification> result){
onDataLoaded(result, !result.isEmpty());
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
}
})
.exec(accountID);
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=list.getAdapter().getItemCount());
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
expandedTitle=(TextView) LayoutInflater.from(getActivity()).inflate(R.layout.expanded_title_medium, list, false);
expandedTitle.setText(getTitle());
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(expandedTitle));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(recyclerView.getChildCount()==0)
return;
float fraction;
View topChild=recyclerView.getChildAt(0);
if(recyclerView.getChildAdapterPosition(topChild)>0){
fraction=1;
}else{
fraction=(-topChild.getTop())/(float)(topChild.getHeight()-topChild.getPaddingBottom());
}
expandedTitle.setAlpha(1f-fraction);
toolbarTitleView.setAlpha(fraction);
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notification_request, menu);
MenuItem mute=menu.findItem(R.id.mute);
MenuItem allow=menu.findItem(R.id.allow);
if(choiceMade && allowed){
allow.setIcon(R.drawable.ic_check_wght700_24px);
tintMenuIcon(allow, R.attr.colorM3Primary);
}else{
tintMenuIcon(allow, R.attr.colorM3OnSurfaceVariant);
}
if(choiceMade && !allowed){
mute.setIcon(R.drawable.ic_delete_wght700_24px);
tintMenuIcon(mute, R.attr.colorM3Primary);
}else{
tintMenuIcon(mute, R.attr.colorM3OnSurfaceVariant);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(choiceMade)
return true;
allowed=item.getItemId()==R.id.allow;
new RespondToNotificationRequest(requestID, allowed)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
choiceMade=true;
invalidateOptionsMenu();
E.post(new NotificationRequestRespondedEvent(accountID, requestID));
new Snackbar.Builder(getActivity())
.setText(getString(allowed ? R.string.notifications_allowed : R.string.notifications_muted, account.displayName))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
return true;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
}
return super.buildDisplayItems(n);
}
@Override
protected boolean wantsToolbarMenuIconsTinted(){
return false;
}
private void tintMenuIcon(MenuItem item, int color){
int tintColor=UiUtils.getThemeColor(getActivity(), color);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
Drawable icon=item.getIcon();
if(icon!=null && icon.getColorFilter()==null){
icon=icon.mutate();
icon.setTintList(ColorStateList.valueOf(tintColor));
item.setIcon(icon);
}
}else{
item.setIconTintList(ColorStateList.valueOf(tintColor));
}
}
}

View File

@ -140,11 +140,6 @@ public class AccountTimelineFragment extends StatusListFragment{
return mergeAdapter;
}
@Override
protected int getMainAdapterOffset(){
return super.getMainAdapterOffset()+1;
}
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
return switch(filter){
case DEFAULT -> defaultFilter;

View File

@ -0,0 +1,120 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
protected String maxID;
protected View endMark;
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
if(n.status!=null){
n.status.card=null;
n.status.spoilerText=null;
}
}
if(n.status!=null){
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
}
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
}
private Notification getNotificationByID(String id){
for(Notification n : data){
if(n.id.equals(id))
return n;
}
return null;
}
protected void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0; i<displayItems.size(); i++){
if(n.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
}
}

View File

@ -325,7 +325,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
toolbar.setNavigationContentDescription(R.string.back);
}
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
return mergeAdapter.getPositionForAdapter(adapter);
}

View File

@ -23,6 +23,10 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
@ -32,15 +36,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -66,7 +69,9 @@ import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.ExtendedPopupMenu;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PopupKeyboard;
@ -87,10 +92,12 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@ -131,7 +138,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, languageBtn;
private TextView replyText;
private Button visibilityBtn;
private LinearLayout visibilityBtn;
private TextView visibilityText1, visibilityText2, visibilityCurrentText;
private LinearLayout bottomBar;
private View autocompleteDivider;
@ -271,6 +279,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
visibilityText1=view.findViewById(R.id.visibility_text1);
visibilityText2=view.findViewById(R.id.visibility_text2);
visibilityCurrentText=visibilityText1;
languageBtn=view.findViewById(R.id.btn_language);
replyText=view.findViewById(R.id.reply_text);
@ -280,9 +291,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBtn.setOnClickListener(v->toggleSpoiler());
languageBtn.setOnClickListener(v->showLanguageAlert());
visibilityBtn.setOnClickListener(this::onVisibilityClick);
visibilityBtn.setAccessibilityDelegate(new View.AccessibilityDelegate(){
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName("android.widget.Spinner");
}
});
Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate();
arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
visibilityBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrow, null);
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
public void onIconChanged(int icon){
@ -323,7 +340,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
}
updateVisibilityIcon();
updateVisibilityIcon(false);
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){
@ -909,22 +926,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void onVisibilityClick(View v){
PopupMenu menu=new PopupMenu(getActivity(), v);
menu.inflate(R.menu.compose_visibility);
menu.setOnMenuItemClickListener(item->{
int id=item.getItemId();
if(id==R.id.vis_public){
statusVisibility=StatusPrivacy.PUBLIC;
}else if(id==R.id.vis_followers){
statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){
statusVisibility=StatusPrivacy.DIRECT;
ArrayList<ListItem<StatusPrivacy>> items=new ArrayList<>();
ExtendedPopupMenu menu=new ExtendedPopupMenu(getActivity(), items);
Consumer<ListItem<StatusPrivacy>> onClick=i->{
if(statusVisibility!=i.parentObject){
statusVisibility=i.parentObject;
updateVisibilityIcon(true);
}
item.setChecked(true);
updateVisibilityIcon();
return true;
});
menu.show();
menu.dismiss();
};
items.add(new ListItem<>(R.string.visibility_public, R.string.visibility_subtitle_public, R.drawable.ic_public_24px, StatusPrivacy.PUBLIC, onClick));
items.add(new ListItem<>(R.string.visibility_unlisted, R.string.visibility_subtitle_unlisted, R.drawable.ic_clear_night_24px, StatusPrivacy.UNLISTED, onClick));
items.add(new ListItem<>(R.string.visibility_followers_only, R.string.visibility_subtitle_followers, R.drawable.ic_lock_24px, StatusPrivacy.PRIVATE, onClick));
items.add(new ListItem<>(R.string.visibility_private, R.string.visibility_subtitle_private, R.drawable.ic_alternate_email_24px, StatusPrivacy.DIRECT, onClick));
menu.showAsDropDown(v);
}
private void loadDefaultStatusVisibility(Bundle savedInstanceState){
@ -950,12 +965,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void applyPreferencesForPostVisibility(Preferences prefs, Bundle savedInstanceState){
// Only override the reply visibility if our preference is more private
if(prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)){
// Map unlisted from the API onto public, because we don't have unlisted in the UI
statusVisibility=switch(prefs.postingDefaultVisibility){
case PUBLIC, UNLISTED -> StatusPrivacy.PUBLIC;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
statusVisibility=prefs.postingDefaultVisibility;
}
// A saved privacy setting from a previous compose session wins over all
@ -963,28 +973,45 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
updateVisibilityIcon();
updateVisibilityIcon(false);
}
private void updateVisibilityIcon(){
private void updateVisibilityIcon(boolean animated){
if(getActivity()==null)
return;
if(statusVisibility==null){ // TODO find out why this happens
statusVisibility=StatusPrivacy.PUBLIC;
}
visibilityBtn.setText(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.string.visibility_public;
TextView visibilityText;
if(!animated){
visibilityText=visibilityCurrentText;
}else{
TransitionManager.beginDelayedTransition(visibilityBtn, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds().excludeTarget(TextView.class, true))
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
visibilityText=visibilityCurrentText==visibilityText1 ? visibilityText2 : visibilityText1;
visibilityText.setVisibility(View.VISIBLE);
visibilityCurrentText.setVisibility(View.GONE);
visibilityCurrentText=visibilityText;
}
visibilityText.setText(switch(statusVisibility){
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
});
Drawable icon=getResources().getDrawable(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.drawable.ic_public_20px;
case PUBLIC -> R.drawable.ic_public_20px;
case UNLISTED -> R.drawable.ic_clear_night_20px;
case PRIVATE -> R.drawable.ic_group_20px;
case DIRECT -> R.drawable.ic_alternate_email_20px;
}, getActivity().getTheme()).mutate();
icon.setBounds(0, 0, V.dp(18), V.dp(18));
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
visibilityBtn.setCompoundDrawablesRelative(icon, null, visibilityBtn.getCompoundDrawablesRelative()[2], null);
visibilityText.setCompoundDrawablesRelative(icon, null, null, null);
}
@Override

View File

@ -34,7 +34,6 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
@ -155,7 +154,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
}
@Override
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
return 1;
}

View File

@ -0,0 +1,250 @@
package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotificationRequests;
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.NotificationRequest;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.Snackbar;
import org.parceler.Parcels;
import java.util.HashMap;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class NotificationRequestsFragment extends MastodonRecyclerFragment<NotificationRequest>{
private String accountID;
private String maxID;
private HashMap<String, AccountViewModel> accountViewModels=new HashMap<>();
private View endMark;
private NotificationRequestsAdapter adapter;
public NotificationRequestsFragment(){
super(50);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setTitle(R.string.filtered_notifications);
loadData();
E.register(this);
}
@Override
public void onDestroy(){
E.unregister(this);
super.onDestroy();
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && endMark!=null)
endMark.setVisibility(View.GONE);
currentRequest=new GetNotificationRequests(offset==0 ? null : maxID)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<NotificationRequest> result){
if(data.isEmpty() || refreshing)
accountViewModels.clear();
maxID=result.getNextPageMaxID();
for(NotificationRequest req:result){
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
}
onDataLoaded(result, !TextUtils.isEmpty(maxID));
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
return adapter=new NotificationRequestsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof NotificationRequestViewHolder).setDrawBelowLastItem(true));
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Subscribe
public void onNotificationRequestResponded(NotificationRequestRespondedEvent ev){
if(adapter==null || !ev.accountID.equals(accountID))
return;
for(int i=0;i<data.size();i++){
if(data.get(i).id.equals(ev.requestID)){
data.remove(i);
adapter.notifyItemRemoved(i);
return;
}
}
for(NotificationRequest nr:preloadedData){
if(nr.id.equals(ev.requestID)){
preloadedData.remove(nr);
break;
}
}
}
private class NotificationRequestsAdapter extends UsableRecyclerView.Adapter<NotificationRequestViewHolder> implements ImageLoaderRecyclerAdapter{
public NotificationRequestsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public NotificationRequestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new NotificationRequestViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public void onBindViewHolder(NotificationRequestViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return Objects.requireNonNull(accountViewModels.get(data.get(position).account.id)).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(data.get(position).account.id));
return switch(image){
case 0 -> model.avaRequest;
default -> model.emojiHelper.getImageRequest(image-1);
};
}
}
private class NotificationRequestViewHolder extends BindableViewHolder<NotificationRequest> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name, username, badge;
private final ImageView ava;
private final ImageButton allow, mute;
public NotificationRequestViewHolder(){
super(getActivity(), R.layout.item_notification_request, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
badge=findViewById(R.id.badge);
ava=findViewById(R.id.ava);
allow=findViewById(R.id.btn_allow);
mute=findViewById(R.id.btn_mute);
ava.setOutlineProvider(OutlineProviders.roundedRect(8));
ava.setClipToOutline(true);
allow.setOnClickListener(this::onAllowClick);
mute.setOnClickListener(this::onMuteClick);
}
@SuppressLint("DefaultLocale")
@Override
public void onBind(NotificationRequest item){
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
name.setText(model.parsedName);
username.setText(item.account.getDisplayUsername());
badge.setText(item.notificationsCount>99 ? String.format("%d+", 99) : String.format("%d", item.notificationsCount));
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
if(image==null)
ava.setImageResource(R.drawable.image_placeholder);
else
ava.setImageDrawable(image);
}else{
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
model.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(item.account));
args.putString("requestID", item.id);
Nav.go(getActivity(), AccountNotificationsListFragment.class, args);
}
private void onAllowClick(View v){
acceptOrDecline(true);
}
private void onMuteClick(View v){
acceptOrDecline(false);
}
private void acceptOrDecline(boolean accept){
new RespondToNotificationRequest(item.id, accept)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
int pos=data.indexOf(item);
data.remove(pos);
adapter.notifyItemRemoved(pos);
new Snackbar.Builder(getActivity())
.setText(getString(accept ? R.string.notifications_allowed : R.string.notifications_muted, item.account.displayName))
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
}
}

View File

@ -1,57 +1,70 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.GetNotificationsPolicy;
import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolicy;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.NotificationsPolicy;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.GenericListItemsViewController;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
public class NotificationsListFragment extends BaseNotificationsListFragment{
private boolean onlyMentions;
private String maxID;
private View tabBar;
private View mentionsTab, allTab;
private View endMark;
private String unreadMarker, realUnreadMarker;
private MenuItem markAllReadItem;
private boolean reloadingFromCache;
private ListItem<Void> requestsItem=new ListItem<>(R.string.filtered_notifications, 0, R.drawable.ic_inventory_2_24px, i->openNotificationRequests());
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
private NotificationsPolicy lastPolicy;
@Override
public void onCreate(Bundle savedInstanceState){
@ -74,43 +87,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
setTitle(R.string.notifications);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
if(n.status!=null){
n.status.card=null;
n.status.spoilerText=null;
}
}
if(n.status!=null){
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account);
}
@Override
protected void doLoadData(int offset, int count){
if(!refreshing && !reloadingFromCache)
endMark.setVisibility(View.GONE);
if(offset==0)
reloadPolicy();
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@ -142,30 +124,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
resetUnreadBackground();
}
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status.clone()));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}else{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(n.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
tabBar=view.findViewById(R.id.tabbar);
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
View tabBarItself=view.findViewById(R.id.tabbar_inner);
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
@ -215,14 +177,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return views;
}
private Notification getNotificationByID(String id){
for(Notification n:data){
if(n.id.equals(id))
return n;
}
return null;
}
@Subscribe
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
@ -249,25 +203,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
private void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(n.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(n.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount()) || holder.getAbsoluteAdapterPosition()<requestsItems.size();
}
private void onTabClick(View v){
@ -285,34 +223,34 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
}
@Override
protected View onCreateFooterView(LayoutInflater inflater){
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
endMark=v.findViewById(R.id.end_mark);
endMark.setVisibility(View.GONE);
return v;
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
markAllReadItem=menu.findItem(R.id.mark_all_read);
MenuItem filters=menu.findItem(R.id.filters);
filters.setVisible(lastPolicy!=null);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.mark_all_read){
int id=item.getItemId();
if(id==R.id.mark_all_read){
markAsRead();
resetUnreadBackground();
}else if(id==R.id.filters){
showFiltersAlert();
}
return true;
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(requestsRowAdapter);
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void markAsRead(){
if(data.isEmpty())
return;
@ -366,4 +304,93 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
return true;
}
private void updatePolicy(NotificationsPolicy policy){
int count=policy.summary==null ? 0 : policy.summary.pendingRequestsCount;
boolean isShown=!requestsItems.isEmpty();
boolean needShow=count>0;
if(isShown && !needShow){
requestsItems.clear();
requestsRowAdapter.notifyItemRemoved(0);
}else if(!isShown && needShow){
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
requestsItems.add(requestsItem);
requestsRowAdapter.notifyItemInserted(0);
}else if(isShown){
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
requestsRowAdapter.notifyItemChanged(0);
}
lastPolicy=policy;
invalidateOptionsMenu();
}
private void reloadPolicy(){
new GetNotificationsPolicy()
.setCallback(new Callback<>(){
@Override
public void onSuccess(NotificationsPolicy policy){
updatePolicy(policy);
}
@Override
public void onError(ErrorResponse errorResponse){
}
})
.exec(accountID);
}
private void showFiltersAlert(){
GenericListItemsViewController<Void> controller=new GenericListItemsViewController<>(getActivity());
Consumer<CheckableListItem<Void>> toggler=item->{
item.toggle();
controller.rebindItem(item);
};
CheckableListItem<Void> followingItem, followersItem, newAccountsItem, mentionsItem;
List<ListItem<Void>> items=List.of(
followingItem=new CheckableListItem<>(R.string.notification_filter_following, R.string.notification_filter_following_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowing, toggler, true),
followersItem=new CheckableListItem<>(R.string.notification_filter_followers, R.string.notification_filter_followers_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowers, toggler, true),
newAccountsItem=new CheckableListItem<>(R.string.notification_filter_new_accounts, R.string.notification_filter_new_accounts_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNewAccounts, toggler, true),
mentionsItem=new CheckableListItem<>(R.string.notification_filter_mentions, R.string.notification_filter_mentions_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterPrivateMentions, toggler, true)
);
controller.setItems(items);
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.filter_notifications)
.setView(controller.getView())
.setPositiveButton(R.string.save, null)
.show();
Button btn=dlg.getButton(Dialog.BUTTON_POSITIVE);
btn.setOnClickListener(v->{
UiUtils.showProgressForAlertButton(btn, true);
NotificationsPolicy newPolicy=new NotificationsPolicy();
newPolicy.filterNotFollowing=followingItem.checked;
newPolicy.filterNotFollowers=followersItem.checked;
newPolicy.filterNewAccounts=newAccountsItem.checked;
newPolicy.filterPrivateMentions=mentionsItem.checked;
new SetNotificationsPolicy(newPolicy)
.setCallback(new Callback<>(){
@Override
public void onSuccess(NotificationsPolicy policy){
updatePolicy(policy);
dlg.dismiss();
}
@Override
public void onError(ErrorResponse errorResponse){
Activity activity=getActivity();
if(activity==null)
return;
UiUtils.showProgressForAlertButton(btn, false);
errorResponse.showToast(activity);
}
})
.exec(accountID);
});
}
private void openNotificationRequests(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), NotificationRequestsFragment.class, args);
}
}

View File

@ -198,6 +198,7 @@ public class ProfileFeaturedFragment extends BaseStatusListFragment<SearchResult
private void showAllFeaturedHashtags(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
ArrayList<Parcelable> tags=featuredTags.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new));
args.putParcelableArrayList("hashtags", tags);
Nav.go(getActivity(), FeaturedHashtagsListFragment.class, args);

View File

@ -20,7 +20,6 @@ import android.os.Build;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.SpannedString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
@ -29,7 +28,6 @@ import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -67,6 +65,7 @@ import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
@ -136,6 +135,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
private ImageButton qrCodeButton;
private ProgressBar innerProgress;
private View actions;
private Account account;
private String accountID;
@ -219,6 +220,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
scrollableContent=content.findViewById(R.id.scrollable_content);
qrCodeButton=content.findViewById(R.id.qr_code);
innerProgress=content.findViewById(R.id.profile_progress);
actions=content.findViewById(R.id.profile_actions);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
@ -306,6 +309,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
username.setOnLongClickListener(v->{
if(account==null)
return true;
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
@ -331,7 +336,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
usernameDomain.setOnClickListener(v->{
if(account==null)
return;
new DecentralizationExplainerSheet(getActivity(), accountID, account).show();
});
qrCodeButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
@ -462,6 +471,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return true;
}
});
if(!loaded)
bindHeaderViewForPreviewMaybe();
}
@Override
@ -506,7 +517,41 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private void bindHeaderViewForPreviewMaybe(){
if(loaded)
return;
String username=getArguments().getString("accountUsername");
String domain=getArguments().getString("accountDomain");
if(TextUtils.isEmpty(username) || TextUtils.isEmpty(domain))
return;
content.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
errorView.setVisibility(View.GONE);
innerProgress.setVisibility(View.VISIBLE);
this.username.setText(username);
name.setText(username);
usernameDomain.setText(domain);
avatar.setImageResource(R.drawable.image_placeholder);
cover.setImageResource(R.drawable.image_placeholder);
actions.setVisibility(View.GONE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
tabsDivider.setVisibility(View.GONE);
}
private void bindHeaderView(){
if(innerProgress.getVisibility()==View.VISIBLE){
TransitionManager.beginDelayedTransition(contentView, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.excludeChildren(actions, true)
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
);
innerProgress.setVisibility(View.GONE);
countersLayout.setVisibility(View.VISIBLE);
actions.setVisibility(View.VISIBLE);
tabsDivider.setVisibility(View.VISIBLE);
}
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
@ -635,6 +680,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
menu.findItem(R.id.block_domain).setVisible(false);
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
if(relationship.following){
MenuItem notifications=menu.findItem(R.id.notifications);
notifications.setVisible(true);
notifications.setIcon(relationship.notifying ? R.drawable.ic_notifications_fill1_24px : R.drawable.ic_notifications_24px);
notifications.setTitle(getString(relationship.notifying ? R.string.disable_new_post_notifications : R.string.enable_new_post_notifications, account.getDisplayUsername()));
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P && !UiUtils.isEMUI()){
menu.setGroupDividerEnabled(true);
}
@ -663,7 +715,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
updateRelationship();
}, this::updateRelationship);
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
@ -693,6 +745,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
}else if(id==R.id.notifications){
new SetAccountFollowed(account.id, true, relationship.showingReblogs, !relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
updateRelationship(result);
new Snackbar.Builder(getActivity())
.setText(result.notifying ? R.string.new_post_notifications_enabled : R.string.new_post_notifications_disabled)
.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
return true;
}
@ -1058,6 +1128,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void onAvatarClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(AVATAR_RESULT);
}else{
@ -1071,6 +1143,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
private void onCoverClick(View v){
if(account==null)
return;
if(isInEditMode){
startImagePicker(COVER_RESULT);
}else{

View File

@ -316,8 +316,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceRedirectRequest=null;
loadingInstanceDomain=null;
Activity a=getActivity();
if(a==null)
if(a==null) {
response.close();
return;
}
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->{

View File

@ -133,7 +133,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
}
numRunningFollowRequests++;
String id=accountIdsToFollow.remove(0);
new SetAccountFollowed(id, true, true)
new SetAccountFollowed(id, true, true, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@ -134,7 +134,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
@Override
protected int getMainAdapterOffset(){
public int getMainAdapterOffset(){
return 1;
}

View File

@ -34,7 +34,6 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ReportDoneFragment extends MastodonToolbarFragment{
@ -177,7 +176,7 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
}
private void onUnfollowClick(){
new SetAccountFollowed(reportAccount.id, false, false)
new SetAccountFollowed(reportAccount.id, false, false, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@ -100,15 +100,19 @@ public class SettingsServerAboutFragment extends LoaderFragment{
scroller.setClipToPadding(false);
scroller.addView(scrollingLayout);
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
if(!TextUtils.isEmpty(instance.thumbnail)){
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
}else{
scrollingLayout.setPadding(0, V.dp(24), 0, 0);
}
boolean needDivider=false;
if(instance.contactAccount!=null){

View File

@ -0,0 +1,27 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import java.time.Instant;
public class NotificationRequest extends BaseModel{
@RequiredField
public String id;
@RequiredField
public Instant createdAt;
@RequiredField
public Instant updatedAt;
public int notificationsCount;
@RequiredField
public Account account;
public Status lastStatus;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
account.postprocess();
if(lastStatus!=null)
lastStatus.postprocess();
}
}

View File

@ -0,0 +1,14 @@
package org.joinmastodon.android.model;
public class NotificationsPolicy extends BaseModel{
public boolean filterNewAccounts;
public boolean filterNotFollowers;
public boolean filterNotFollowing;
public boolean filterPrivateMentions;
public Summary summary;
public static class Summary{
public int pendingNotificationsCount;
public int pendingRequestsCount;
}
}

View File

@ -58,7 +58,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
public boolean favourited;
public boolean reblogged;
public boolean muted;
public Boolean muted;
public boolean bookmarked;
public Boolean pinned;

View File

@ -25,6 +25,10 @@ public class AccountViewModel{
public final String verifiedLink;
public AccountViewModel(Account account, String accountID){
this(account, accountID, true);
}
public AccountViewModel(Account account, String accountID, boolean needBio){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
@ -32,9 +36,13 @@ public class AccountViewModel{
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
else
parsedName=account.displayName;
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
ssb.append(parsedBio);
if(needBio){
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
ssb.append(parsedBio);
}else{
parsedBio=null;
}
emojiHelper.setText(ssb);
String verifiedLink=null;
for(AccountField fld:account.fields){

View File

@ -68,6 +68,12 @@ public class ListItem<T>{
this.subtitleRes=subtitleRes;
}
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, T parentObject, Consumer<ListItem<T>> onClick){
this(null, null, iconRes, onClick, parentObject, 0, false);
this.titleRes=titleRes;
this.subtitleRes=subtitleRes;
}
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
this.titleRes=titleRes;

View File

@ -35,8 +35,9 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
this.drawDividerPredicate=drawDividerPredicate;
}
public void setDrawBelowLastItem(boolean drawBelowLastItem){
public DividerItemDecoration setDrawBelowLastItem(boolean drawBelowLastItem){
this.drawBelowLastItem=drawBelowLastItem;
return this;
}
@Override

View File

@ -0,0 +1,61 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ExtendedPopupMenu extends PopupWindow{
private UsableRecyclerView list;
public <T> ExtendedPopupMenu(Context context, List<ListItem<T>> items){
super(context, null, 0, R.style.Widget_Mastodon_PopupMenu);
setWidth(V.dp(200));
setElevation(V.dp(3));
setOutsideTouchable(true);
setFocusable(true);
setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
list=new UsableRecyclerView(context);
list.setLayoutManager(new LinearLayoutManager(context));
list.setAdapter(new ReducedPaddingItemsAdapter<>(items));
list.setClipToPadding(false);
setContentView(list);
}
@Override
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity){
super.showAsDropDown(anchor, xoff, yoff, gravity);
View bgView=(View) list.getParent();
list.setPadding(0, bgView.getPaddingTop(), 0, bgView.getPaddingBottom());
bgView.setPadding(0, 0, 0, 0);
}
private static class ReducedPaddingItemsAdapter<T> extends GenericListItemsAdapter<T>{
public ReducedPaddingItemsAdapter(List<ListItem<T>> listItems){
super(listItems);
}
@NonNull
@Override
public ListItemViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
ListItemViewHolder<?> holder=super.onCreateViewHolder(parent, viewType);
int padH=V.dp(12), padV=V.dp(8);
holder.itemView.setPadding(padH, padV, padH, padV);
View icon=holder.itemView.findViewById(R.id.icon);
((ViewGroup.MarginLayoutParams)icon.getLayoutParams()).setMarginEnd(padH);
return holder;
}
}
}

View File

@ -50,6 +50,11 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
helpButton.setSelected(helpText.getVisibility()==View.VISIBLE);
});
setCustomTitle(titleLayout);
}else if(!TextUtils.isEmpty(title)){
View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title, null);
TextView title=titleLayout.findViewById(R.id.title);
title.setText(this.title);
setCustomTitle(titleLayout);
}
alert=super.create();

View File

@ -58,7 +58,7 @@ public class PhotoLayoutHelper{
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
if(cnt==2){
if(allAreWide && avgRatio>1.4*maxRatio && (ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
if(allAreWide && avgRatio>1.4*maxRatio && Math.abs(ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
float h=Math.max(Math.min(MAX_WIDTH/ratios.get(0), Math.min(MAX_WIDTH/ratios.get(1), (MAX_HEIGHT-GAP)/2.0f)), MIN_HEIGHT/2f);
result.width=MAX_WIDTH;
@ -69,7 +69,23 @@ public class PhotoLayoutHelper{
new TiledLayoutResult.Tile(1, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 0, 1)
};
}else if(allAreWide || allAreSquare){ // next to each other, same ratio
}else if(allAreWide){ // two wide photos, one above the other, different ratios
result.width=MAX_WIDTH;
float h0=MAX_WIDTH/ratios.get(0);
float h1=MAX_WIDTH/ratios.get(1);
if(h0+h1<MIN_HEIGHT){
float prevTotalHeight=h0+h1;
h0=MIN_HEIGHT*(h0/prevTotalHeight);
h1=MIN_HEIGHT*(h1/prevTotalHeight);
}
result.height=Math.round(h0+h1+GAP);
result.rowSizes=new int[]{Math.round(h0), Math.round(h1)};
result.columnSizes=new int[]{MAX_WIDTH};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, 0, 0),
new TiledLayoutResult.Tile(1, 1, 0, 1)
};
}else if(allAreSquare){ // next to each other, same ratio
float w=((MAX_WIDTH-GAP)/2);
float h=Math.max(Math.min(w/ratios.get(0), Math.min(w/ratios.get(1), MAX_HEIGHT)), MIN_HEIGHT);

View File

@ -6,6 +6,7 @@ import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
@ -19,6 +20,9 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
@ -48,6 +52,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private final ImageView share;
private final ColorStateList buttonColors;
private final View replyBtn, boostBtn, favoriteBtn, shareBtn;
private final PopupMenu boostLongTapMenu, favoriteLongTapMenu;
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@Override
@ -97,11 +102,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
replyBtn.setOnClickListener(this::onReplyClick);
replyBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
boostBtn.setOnClickListener(this::onBoostClick);
boostBtn.setOnLongClickListener(this::onBoostLongClick);
boostBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
favoriteBtn.setOnClickListener(this::onFavoriteClick);
favoriteBtn.setOnLongClickListener(this::onFavoriteLongClick);
favoriteBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
shareBtn.setOnClickListener(this::onShareClick);
shareBtn.setAccessibilityDelegate(buttonAccessibilityDelegate);
favoriteLongTapMenu=new PopupMenu(activity, favoriteBtn);
favoriteLongTapMenu.inflate(R.menu.favorite_longtap);
favoriteLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
boostLongTapMenu=new PopupMenu(activity, boostBtn);
boostLongTapMenu.inflate(R.menu.boost_longtap);
boostLongTapMenu.setOnMenuItemClickListener(this::onLongTapMenuItemSelected);
}
@Override
@ -172,6 +186,45 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
UiUtils.openSystemShareSheet(v.getContext(), item.status);
}
private boolean onBoostLongClick(View v){
MenuItem boost=boostLongTapMenu.getMenu().findItem(R.id.boost);
boost.setTitle(item.status.reblogged ? R.string.undo_reblog : R.string.button_reblog);
boostLongTapMenu.show();
return true;
}
private boolean onFavoriteLongClick(View v){
MenuItem favorite=favoriteLongTapMenu.getMenu().findItem(R.id.favorite);
MenuItem bookmark=favoriteLongTapMenu.getMenu().findItem(R.id.bookmark);
favorite.setTitle(item.status.favourited ? R.string.undo_favorite : R.string.button_favorite);
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
favoriteLongTapMenu.show();
return true;
}
private boolean onLongTapMenuItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.favorite){
onFavoriteClick(null);
}else if(id==R.id.boost){
onBoostClick(null);
}else if(id==R.id.bookmark){
AccountSessionManager.getInstance().getAccount(this.item.accountID).getStatusInteractionController().setBookmarked(this.item.status, !this.item.status.bookmarked);
}else if(id==R.id.view_favorites){
startAccountListFragment(StatusFavoritesListFragment.class);
}else if(id==R.id.view_boosts){
startAccountListFragment(StatusReblogsListFragment.class);
}
return true;
}
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putParcelable("status", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), cls, args);
}
private int descriptionForId(int id){
if(id==R.id.reply_btn)
return R.string.button_reply;

View File

@ -25,11 +25,13 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
import org.joinmastodon.android.api.requests.statuses.SetStatusConversationMuted;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
@ -117,6 +119,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private final TextView name, timeAndUsername, extraText;
private final ImageView avatar, more;
private final PopupMenu optionsMenu;
private final View clickableThing;
public Holder(Activity activity, ViewGroup parent){
this(activity, R.layout.display_item_header, parent);
@ -129,7 +132,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
extraText=findViewById(R.id.extra_text);
avatar.setOnClickListener(this::onAvaClick);
clickableThing=findViewById(R.id.clickable_thing);
clickableThing.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
avatar.setClipToOutline(true);
more.setOnClickListener(this::onMoreClick);
@ -228,6 +232,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
})
.wrapProgress(activity, R.string.loading, true)
.exec(item.accountID);
}else if(id==R.id.mute_conversation){
new SetStatusConversationMuted(item.status.id, !item.status.muted)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
// TODO snackbar?
item.status.muted=result.muted;
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.loading, true)
.exec(item.accountID);
}
return true;
});
@ -244,7 +264,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
timeAndUsername.setText(time+" · @"+item.user.acct);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : V.dp(4));
if(TextUtils.isEmpty(item.extraText)){
extraText.setVisibility(View.GONE);
}else{
@ -252,8 +272,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
extraText.setText(item.extraText);
}
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
avatar.setClickable(!item.inset);
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
clickableThing.setClickable(!item.inset);
clickableThing.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
}
@Override
@ -314,6 +334,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
MenuItem follow=menu.findItem(R.id.follow);
MenuItem bookmark=menu.findItem(R.id.bookmark);
MenuItem pin=menu.findItem(R.id.pin);
MenuItem muteConversation=menu.findItem(R.id.mute_conversation);
if(item.status!=null){
bookmark.setVisible(true);
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
@ -340,6 +361,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
report.setTitle(item.parentFragment.getString(R.string.report_user, account.displayName));
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.displayName));
}
if(item.status.muted!=null){
muteConversation.setVisible(isOwnPost || item.parentFragment instanceof NotificationsListFragment);
muteConversation.setTitle(item.status.muted ? R.string.unmute_conversation : R.string.mute_conversation);
}else{
muteConversation.setVisible(false);
}
menu.findItem(R.id.add_to_list).setVisible(relationship!=null && relationship.following);
}
}

View File

@ -173,7 +173,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
}
controllers.add(c);
if (item.status.translation != null){
if (item.status.translation!=null && item.status.translation.mediaAttachments!=null){
if(item.status.translationState==Status.TranslationState.SHOWN){
if(!item.translatedAttachments.containsKey(att.id)){
Optional<Translation.MediaAttachment> translatedAttachment=Arrays.stream(item.status.translation.mediaAttachments).filter(mediaAttachment->mediaAttachment.id.equals(att.id)).findFirst();

View File

@ -125,7 +125,8 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
public void onBind(NotificationHeaderStatusDisplayItem item){
text.setText(item.text);
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
// TODO use real icons
if(item.notification.type!=Notification.Type.POLL)
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.notification.account.acct));
icon.setImageResource(switch(item.notification.type){
case FAVORITE -> R.drawable.ic_star_fill1_24px;
case REBLOG -> R.drawable.ic_repeat_fill1_24px;

View File

@ -151,10 +151,12 @@ public abstract class StatusDisplayItem{
if(!imageAttachments.isEmpty()){
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
mediaGrid.sensitiveRevealed=false;
}else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia){
mediaGrid.sensitiveRevealed=true;
}
contentItems.add(mediaGrid);
}
for(Attachment att:statusForContent.mediaAttachments){

View File

@ -94,7 +94,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
text.setTextIsSelectable(item.textSelectable);
text.setInvalidateOnEveryFrame(false);
itemView.setClickable(false);
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(16), text.getPaddingRight(), text.getPaddingBottom());
text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(12), text.getPaddingRight(), text.getPaddingBottom());
text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface));
updateTranslation(false);
}

View File

@ -69,6 +69,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
backButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
backButton.setElevation(V.dp(2));
backButton.setAlpha(0f);
backButton.setContentDescription(context.getString(R.string.back));
backButton.setOnClickListener(v->{
listener.onDismissEntireViewer();
dismiss();
@ -82,6 +83,7 @@ public class PhotoViewerInfoSheet extends BottomSheet{
infoButton.setElevation(V.dp(2));
infoButton.setAlpha(0f);
infoButton.setSelected(true);
infoButton.setContentDescription(context.getString(R.string.info));
infoButton.setOnClickListener(v->dismiss());
FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(V.dp(48), V.dp(48));

View File

@ -86,6 +86,7 @@ public class HtmlParser{
// Hashtags in remote posts have remote URLs, these have local URLs so they don't match.
// Map<String, String> tagsByUrl=tags.stream().collect(Collectors.toMap(t->t.url, t->t.name));
Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
Map<String, Mention> mentionsByID=mentions.stream().distinct().collect(Collectors.toMap(m->m.id, Function.identity()));
final SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
@ -115,6 +116,7 @@ public class HtmlParser{
if(id!=null){
linkType=LinkSpan.Type.MENTION;
href=id;
linkObject=mentionsByID.get(id);
}else{
linkType=LinkSpan.Type.URL;
}

View File

@ -2,9 +2,12 @@ package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.ui.utils.UiUtils;
public class LinkSpan extends CharacterStyle {
@ -39,7 +42,21 @@ public class LinkSpan extends CharacterStyle {
public void onClick(Context context){
switch(getType()){
case URL -> UiUtils.openURL(context, accountID, link, parentObject);
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
case MENTION -> {
String username, domain;
if(linkObject instanceof Mention m && !TextUtils.isEmpty(m.acct)){
String[] parts=m.acct.split("@", 2);
username=parts[0];
if(parts.length==2){
domain=parts[1];
}else{
domain=AccountSessionManager.get(accountID).domain;
}
}else{
username=domain=null;
}
UiUtils.openProfileByID(context, accountID, link, username, domain);
}
case HASHTAG -> {
if(linkObject instanceof Hashtag ht)
UiUtils.openHashtagTimeline(context, accountID, ht);

View File

@ -37,7 +37,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
for(int i=0; i<parent.getChildCount(); i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
pos=holder.getAbsoluteAdapterPosition();
pos=holder.getAbsoluteAdapterPosition()-listFragment.getMainAdapterOffset();
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
if(inset){
if(rect.isEmpty()){
@ -82,7 +82,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
boolean inset=sdi.getItem().inset;
int pos=holder.getAbsoluteAdapterPosition();
int pos=holder.getAbsoluteAdapterPosition()-listFragment.getMainAdapterOffset();
if(inset){
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;

View File

@ -26,8 +26,6 @@ import android.os.SystemClock;
import android.os.ext.SdkExtensions;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.system.ErrnoException;
import android.system.Os;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
@ -37,7 +35,6 @@ import android.transition.ChangeScroll;
import android.transition.Fade;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
@ -47,17 +44,14 @@ import android.view.ViewGroup;
import android.view.WindowInsets;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.E;
import org.joinmastodon.android.FileProvider;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
@ -359,9 +353,17 @@ public class UiUtils{
}
public static void openProfileByID(Context context, String selfID, String id){
openProfileByID(context, selfID, id, null, null);
}
public static void openProfileByID(Context context, String selfID, String id, String username, String domain){
Bundle args=new Bundle();
args.putString("account", selfID);
args.putString("profileAccountID", id);
if(username!=null && domain!=null){
args.putString("accountUsername", username);
args.putString("accountDomain", domain);
}
Nav.go((Activity)context, ProfileFragment.class, args);
}
@ -590,7 +592,7 @@ public class UiUtils{
}else{
Runnable action=()->{
progressCallback.accept(true);
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@ -313,7 +313,12 @@ public class ComposeMediaViewController{
int maxSize=0;
String contentType=fragment.getActivity().getContentResolver().getType(attachment.uri);
if(contentType!=null && contentType.startsWith("image/")){
maxSize=2_073_600; // TODO get this from instance configuration when it gets added there
Instance instance=fragment.instance;
if(instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.imageMatrixLimit>0){
maxSize=instance.configuration.mediaAttachments.imageMatrixLimit;
}else{
maxSize=2_073_600;
}
}
attachment.progressBar.setProgress(0);
attachment.speedTracker.reset();

View File

@ -0,0 +1,60 @@
package org.joinmastodon.android.ui.viewcontrollers;
import android.content.Context;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.viewholders.CheckableListItemViewHolder;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import java.util.List;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.views.UsableRecyclerView;
public class GenericListItemsViewController<T>{
private UsableRecyclerView list;
private List<ListItem<T>> items;
private GenericListItemsAdapter<T> adapter;
private Context context;
public GenericListItemsViewController(Context context, List<ListItem<T>> items){
this.context=context;
setItems(items);
}
public GenericListItemsViewController(Context context){
this.context=context;
}
public void setItems(List<ListItem<T>> items){
if(this.items!=null)
throw new IllegalStateException("items already set");
this.items=items;
adapter=new GenericListItemsAdapter<>(items);
list=new UsableRecyclerView(context);
list.setLayoutManager(new LinearLayoutManager(context));
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(context, R.attr.colorM3OutlineVariant, 1, 16, 16, vh->(vh instanceof SimpleListItemViewHolder ivh && ivh.getItem().dividerAfter) || (vh instanceof CheckableListItemViewHolder cvh && cvh.getItem().dividerAfter)));
list.setItemAnimator(new BetterItemAnimator());
}
public GenericListItemsAdapter<T> getAdapter(){
return adapter;
}
public View getView(){
return list;
}
public void rebindItem(ListItem<?> item){
if(list.findViewHolderForAdapterPosition(items.indexOf(item)) instanceof ListItemViewHolder<?> holder){
holder.rebind();
}
}
}

View File

@ -3,8 +3,6 @@ package org.joinmastodon.android.ui.viewholders;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
@ -15,7 +13,6 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.ImageView;
@ -254,7 +251,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
bindRelationship();
}, this::updateRelationship);
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<set>
<objectAnimator
android:duration="100"
android:propertyName="alpha"
android:valueTo="0.85"
android:valueFrom="1.0"
android:valueType="floatType"/>
<objectAnimator
android:duration="100"
android:propertyName="textColor"
android:valueTo="#80ffffff"
android:valueFrom="#fff"
android:valueType="colorType"/>
</set>
</item>
<item>
<set>
<objectAnimator
android:duration="200"
android:propertyName="alpha"
android:valueTo="1.0"
android:valueFrom="0.85"
android:valueType="floatType"/>
<objectAnimator
android:duration="200"
android:propertyName="textColor"
android:valueTo="#fff"
android:valueFrom="#80ffffff"
android:valueType="colorType"/>
</set>
</item>
</selector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSurfaceVariant" android:state_enabled="true"/>
<item android:color="?colorM3OnSurfaceVariant" android:alpha="0.38"/>
<item android:color="?colorM3OnSurface" android:alpha="0.38"/>
</selector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorM3Primary"/>
<corners android:radius="100dp"/>
<stroke android:color="?colorM3Background" android:width="1dp"/>
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_primary_overlay">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="14dp"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M9.25,16V6.875L5.062,11.062L4,10L10,4L16,10L14.938,11.062L10.75,6.875V16Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M9.55,18.8 L3.05,12.3 5.3,10.05 9.55,14.3 18.7,5.15 20.95,7.4Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M10,16.583Q11.125,16.583 12.135,16.26Q13.146,15.938 14.021,15.271Q12.229,14.604 10.865,13.427Q9.5,12.25 8.656,10.74Q7.812,9.229 7.542,7.448Q7.271,5.667 7.667,3.812Q5.771,4.5 4.594,6.177Q3.417,7.854 3.417,10Q3.417,12.75 5.333,14.667Q7.25,16.583 10,16.583ZM10,18.333Q8.271,18.333 6.75,17.677Q5.229,17.021 4.104,15.896Q2.979,14.771 2.323,13.25Q1.667,11.729 1.667,10Q1.667,7 3.635,4.729Q5.604,2.458 8.521,1.854Q9.208,1.708 9.51,2.135Q9.812,2.562 9.562,3.312Q9.021,5 9.198,6.708Q9.375,8.417 10.177,9.875Q10.979,11.333 12.323,12.406Q13.667,13.479 15.438,13.917Q16.208,14.104 16.427,14.594Q16.646,15.083 16.167,15.604Q15.021,16.896 13.438,17.615Q11.854,18.333 10,18.333Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,20Q13.35,20 14.613,19.562Q15.875,19.125 16.925,18.3Q15.475,17.775 13.863,16.612Q12.25,15.45 11.025,13.7Q9.8,11.95 9.238,9.637Q8.675,7.325 9.275,4.5Q6.95,5.325 5.475,7.362Q4,9.4 4,12Q4,15.325 6.338,17.663Q8.675,20 12,20ZM12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,8.325 4.312,5.525Q6.625,2.725 10.5,2.125Q11.225,2 11.538,2.462Q11.85,2.925 11.575,3.675Q10.825,5.775 11.05,7.912Q11.275,10.05 12.275,11.85Q13.275,13.65 14.963,14.962Q16.65,16.275 18.825,16.75Q19.625,16.925 19.837,17.425Q20.05,17.925 19.575,18.475Q18.2,20.1 16.238,21.05Q14.275,22 12,22Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6.725,21.85Q5.4,21.85 4.488,20.938Q3.575,20.025 3.575,18.7V6.275H2V3.125H8.425V1.55H15.525V3.125H22V6.275H20.425V18.7Q20.425,20.025 19.513,20.938Q18.6,21.85 17.275,21.85ZM17.275,6.275H6.725V18.7Q6.725,18.7 6.725,18.7Q6.725,18.7 6.725,18.7H17.275Q17.275,18.7 17.275,18.7Q17.275,18.7 17.275,18.7ZM8.55,16.975H11.125V7.975H8.55ZM12.875,16.975H15.45V7.975H12.875ZM6.725,6.275V18.7Q6.725,18.7 6.725,18.7Q6.725,18.7 6.725,18.7Q6.725,18.7 6.725,18.7Q6.725,18.7 6.725,18.7Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,20V8.7Q2.575,8.425 2.288,8Q2,7.575 2,7V4Q2,3.175 2.588,2.587Q3.175,2 4,2H20Q20.825,2 21.413,2.587Q22,3.175 22,4V7Q22,7.575 21.712,8Q21.425,8.425 21,8.7V20Q21,20.825 20.413,21.413Q19.825,22 19,22H5Q4.175,22 3.587,21.413Q3,20.825 3,20ZM5,9V20Q5,20 5,20Q5,20 5,20H19Q19,20 19,20Q19,20 19,20V9ZM20,7Q20,7 20,7Q20,7 20,7V4Q20,4 20,4Q20,4 20,4H4Q4,4 4,4Q4,4 4,4V7Q4,7 4,7Q4,7 4,7ZM9,14H15V12H9ZM5,20Q5,20 5,20Q5,20 5,20V9V20Q5,20 5,20Q5,20 5,20Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,22Q5.175,22 4.588,21.413Q4,20.825 4,20V10Q4,9.175 4.588,8.587Q5.175,8 6,8H7V6Q7,3.925 8.463,2.462Q9.925,1 12,1Q14.075,1 15.538,2.462Q17,3.925 17,6V8H18Q18.825,8 19.413,8.587Q20,9.175 20,10V20Q20,20.825 19.413,21.413Q18.825,22 18,22ZM9,8H15V6Q15,4.75 14.125,3.875Q13.25,3 12,3Q10.75,3 9.875,3.875Q9,4.75 9,6ZM6,20H18Q18,20 18,20Q18,20 18,20V10Q18,10 18,10Q18,10 18,10H6Q6,10 6,10Q6,10 6,10V20Q6,20 6,20Q6,20 6,20ZM12,17Q12.825,17 13.413,16.413Q14,15.825 14,15Q14,14.175 13.413,13.587Q12.825,13 12,13Q11.175,13 10.588,13.587Q10,14.175 10,15Q10,15.825 10.588,16.413Q11.175,17 12,17ZM12,15Q12,15 12,15Q12,15 12,15Q12,15 12,15Q12,15 12,15Q12,15 12,15Q12,15 12,15Q12,15 12,15Q12,15 12,15Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3,19V17H9V19ZM3,7V5H13V7ZM11,21V15H13V17H21V19H13V21ZM7,15V13H3V11H7V9H9V15ZM11,13V11H21V13ZM15,9V3H17V5H21V7H17V9Z"/>
</vector>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?android:windowTitleStyle"
tools:text="Title"/>
</LinearLayout>

View File

@ -14,5 +14,6 @@
android:includeFontPadding="false"
android:background="@drawable/bg_image_alt_overlay"
android:text="ALT"
android:stateListAnimator="@animator/alt_badge"
tools:ignore="HardcodedText"
tools:showIn="@layout/display_item_photo" />

View File

@ -4,8 +4,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingLeft="16dp">
android:paddingHorizontal="16dp"
android:paddingBottom="4dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/more"
@ -21,6 +22,18 @@
android:contentDescription="@string/more_options"
android:src="@drawable/ic_more_vert_20px" />
<View
android:id="@+id/clickable_thing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="-4dp"
android:layout_marginVertical="-4dp"
android:layout_alignLeft="@id/avatar"
android:layout_alignRight="@id/time_and_username"
android:layout_alignTop="@id/avatar"
android:layout_alignBottom="@id/avatar"
android:background="@drawable/bg_status_header"/>
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
@ -28,7 +41,8 @@
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp" />
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"/>
<org.joinmastodon.android.ui.views.HeaderSubtitleLinearLayout
android:id="@+id/name_wrap"
@ -70,6 +84,8 @@
android:layout_height="20dp"
android:layout_below="@id/name_wrap"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/more"
android:layout_marginEnd="8dp"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"

View File

@ -15,9 +15,7 @@
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:lineSpacingExtra="6dp"
android:lineHeight="25sp"
android:textAppearance="@style/m3_body_large"/>
android:lineSpacingExtra="5.25dp"/>
<ViewStub
android:id="@+id/translation_info"

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_headline_small"
android:textColor="?colorM3OnSurface"
android:paddingHorizontal="16dp"
android:paddingBottom="24dp"
android:paddingTop="4dp"
android:maxLines="2"
android:ellipsize="end"
tools:text="Very long title that does not fit on one line"/>

View File

@ -76,21 +76,50 @@
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@Gargron" />
<Button
<LinearLayout
android:id="@+id/btn_visibility"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_below="@id/name"
android:layout_toEndOf="@id/avatar"
android:layout_marginTop="8dp"
android:textAppearance="@style/m3_label_large"
android:background="@drawable/bg_filter_chip"
android:textColor="?colorM3OnSurfaceVariant"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:drawablePadding="8dp"
tools:text="@string/visibility_public"/>
android:paddingHorizontal="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/visibility_text1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3OnSurfaceVariant"
android:drawablePadding="8dp"
android:singleLine="true"
android:gravity="center"
tools:text="@string/visibility_public"/>
<TextView
android:id="@+id/visibility_text2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3OnSurfaceVariant"
android:drawablePadding="8dp"
android:singleLine="true"
android:gravity="center"
android:visibility="gone"
tools:text="@string/visibility_public"/>
<View
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="8dp"
android:layout_gravity="center_vertical"
android:background="@drawable/ic_baseline_arrow_drop_down_18"
android:backgroundTint="?colorM3OnSurface"/>
</LinearLayout>
</RelativeLayout>

View File

@ -32,7 +32,7 @@
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="144dp"
android:background="#808080"
android:background="@drawable/image_placeholder"
android:contentDescription="@string/profile_header"
android:scaleType="centerCrop" />
@ -134,6 +134,14 @@
</RelativeLayout>
<ProgressBar
android:id="@+id/profile_progress"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:visibility="gone"/>
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/bio"
android:layout_width="match_parent"
@ -284,6 +292,7 @@
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<LinearLayout
android:id="@+id/profile_actions"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"

View File

@ -42,17 +42,17 @@
<Button
android:id="@+id/new_posts_btn"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_margin="16dp"
android:layout_height="36dp"
android:layout_margin="8dp"
android:background="@drawable/round_rect"
android:backgroundTint="?colorM3Primary"
android:paddingHorizontal="16dp"
android:paddingHorizontal="12dp"
android:paddingVertical="0dp"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3OnPrimary"
android:drawableStart="@drawable/ic_arrow_upward_24px"
android:drawableStart="@drawable/ic_arrow_upward_20px"
android:drawableTint="?colorM3OnPrimary"
android:drawablePadding="8dp"
android:drawablePadding="4dp"
android:elevation="@dimen/m3_sys_elevation_level4"
android:stateListAnimator="@animator/squish"
android:text="@string/see_new_posts"/>

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp">
<ImageView
android:id="@+id/ava"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginVertical="20dp"
android:layout_marginEnd="12dp"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<TextView
android:id="@+id/badge"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:layout_alignEnd="@id/ava"
android:layout_alignBottom="@id/ava"
android:layout_marginEnd="-4dp"
android:layout_marginBottom="-4dp"
android:minWidth="16dp"
android:gravity="center"
android:background="@drawable/bg_ava_badge"
android:paddingHorizontal="4dp"
android:textAppearance="@style/m3_label_small"
android:textColor="?colorM3OnPrimary"
tools:text="99+"/>
<ImageButton
android:id="@+id/btn_allow"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
style="@style/Widget.Mastodon.M3.Button.Outlined"
android:contentDescription="@string/allow_notifications"
android:src="@drawable/ic_check_24px"/>
<ImageButton
android:id="@+id/btn_mute"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:layout_toStartOf="@id/btn_allow"
android:layout_centerVertical="true"
style="@style/Widget.Mastodon.M3.Button.Outlined"
android:contentDescription="@string/mute_notifications"
android:src="@drawable/ic_delete_24px"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_toEndOf="@id/ava"
android:layout_toStartOf="@id/btn_mute"
android:layout_marginTop="14dp"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_large"
android:gravity="center_vertical"
android:textColor="?colorM3OnSurface"
tools:text="User Name"/>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_below="@id/name"
android:layout_toEndOf="@id/ava"
android:layout_toStartOf="@id/btn_mute"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:gravity="center_vertical"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@username@domain"/>
</RelativeLayout>

View File

@ -7,43 +7,6 @@
android:paddingTop="8dp"
android:paddingHorizontal="8dp">
<org.joinmastodon.android.ui.views.CheckableLinearLayout
android:id="@+id/multiple_choice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="8dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:background="@drawable/bg_rect_4dp_ripple"
android:gravity="center_horizontal">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/poll_multiple"
android:duplicateParentState="true"
android:importantForAccessibility="no"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurface"
android:text="@string/compose_poll_multiple_choice"/>
<FrameLayout
android:layout_width="48dp"
android:layout_height="48dp"
android:duplicateParentState="true">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"
android:focusable="false"
android:duplicateParentState="true"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.CheckableLinearLayout>
<org.joinmastodon.android.ui.views.CheckableLinearLayout
android:id="@+id/single_choice"
android:layout_width="0dp"
@ -81,4 +44,41 @@
</FrameLayout>
</org.joinmastodon.android.ui.views.CheckableLinearLayout>
<org.joinmastodon.android.ui.views.CheckableLinearLayout
android:id="@+id/multiple_choice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="8dp"
android:orientation="vertical"
android:paddingTop="8dp"
android:background="@drawable/bg_rect_4dp_ripple"
android:gravity="center_horizontal">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/poll_multiple"
android:duplicateParentState="true"
android:importantForAccessibility="no"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurface"
android:text="@string/compose_poll_multiple_choice"/>
<FrameLayout
android:layout_width="48dp"
android:layout_height="48dp"
android:duplicateParentState="true">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"
android:focusable="false"
android:duplicateParentState="true"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.CheckableLinearLayout>
</LinearLayout>

View File

@ -56,6 +56,7 @@
android:layout_marginHorizontal="16dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:textIsSelectable="true"
tools:text="A cute black cat"/>
</LinearLayout>

View File

@ -141,6 +141,7 @@
android:layout_marginHorizontal="16dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:textIsSelectable="true"
tools:text="A cute black cat"/>
</LinearLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/boost" android:title="@string/button_reblog"/>
<item android:id="@+id/view_boosts" android:title="@string/view_boosts"/>
</menu>

View File

@ -2,6 +2,8 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/vis_public"
android:title="@string/visibility_public"/>
<item android:id="@+id/vis_unlisted"
android:title="@string/visibility_unlisted"/>
<item android:id="@+id/vis_followers"
android:title="@string/visibility_followers_only"/>
<item android:id="@+id/vis_private"

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/favorite" android:title="@string/button_favorite"/>
<item android:id="@+id/bookmark" android:title="@string/add_bookmark"/>
<item android:id="@+id/view_favorites" android:title="@string/view_favorites"/>
</menu>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/mute"
android:title="@string/mute_notifications"
android:icon="@drawable/ic_delete_24px"
android:checkable="true"
android:showAsAction="always"/>
<item
android:id="@+id/allow"
android:title="@string/allow_notifications"
android:icon="@drawable/ic_check_24px"
android:checkable="true"
android:showAsAction="always"/>
</menu>

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/mark_all_read" android:icon="@drawable/ic_done_all_24px" android:title="@string/mark_all_notifications_read" android:showAsAction="always"/>
<item
android:id="@+id/mark_all_read"
android:icon="@drawable/ic_done_all_24px"
android:showAsAction="always"
android:title="@string/mark_all_notifications_read" />
<item
android:id="@+id/filters"
android:icon="@drawable/ic_tune_24px"
android:showAsAction="always"
android:title="@string/filter_notifications"/>
</menu>

View File

@ -9,6 +9,7 @@
<item android:id="@+id/copy_link" android:title="@string/fallback_menu_item_copy_link"/>
<item android:id="@+id/edit" android:title="@string/edit"/>
<item android:id="@+id/delete" android:title="@string/delete"/>
<item android:id="@+id/mute_conversation" android:title="@string/mute_conversation"/>
</group>
<group android:id="@+id/menu_group2">
<item android:id="@+id/add_to_list" android:title="@string/add_user_to_list"/>

View File

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/notifications"
android:icon="@drawable/ic_tab_notifications"
android:showAsAction="always"
android:visible="false"
tools:ignore="MenuTitle" />
<group android:orderInCategory="1" android:id="@+id/menu_group1">
<item android:id="@+id/share" android:title="@string/share_user"/>
<item android:id="@+id/copy_link" android:title="@string/copy_profile_link"/>

View File

@ -0,0 +1 @@
unqualifiedResLocale=en-US

View File

@ -9,6 +9,7 @@
<string name="preparing_auth">Préparation à lauthentification…</string>
<string name="finishing_auth">Fin de lauthentification…</string>
<string name="user_boosted">%s a boosté</string>
<string name="in_reply_to">en réponse à %s</string>
<string name="notifications">Notifications</string>
<string name="user_followed_you">%s vous suit</string>
<string name="user_sent_follow_request">%s voudrait vous suivre</string>
@ -165,6 +166,8 @@
<string name="save">Enregistrer</string>
<string name="add_alt_text">Ajouter un texte alternatif</string>
<string name="visibility_public">Public</string>
<string name="visibility_followers_only">Abonné·e·s</string>
<string name="visibility_private">Personnes spécifiques</string>
<string name="recent_searches">Récents</string>
<string name="skip">Passer</string>
<string name="notification_type_follow">Nouveaux⋅elles abonné⋅e⋅s</string>
@ -305,6 +308,7 @@
<string name="instance_signup_closed">Ce serveur n\'accepte pas les nouvelles inscriptions.</string>
<string name="text_copied">Copié dans le presse-papier</string>
<string name="add_bookmark">Ajouter aux marque-pages</string>
<string name="remove_bookmark">Supprimer le signet</string>
<string name="bookmarks">Marque-pages</string>
<string name="your_favorites">Vos favoris</string>
<string name="login_title">Bienvenue à nouveau</string>
@ -714,6 +718,15 @@
<string name="mute_conversation">Masquer la conversation</string>
<string name="unmute_conversation">Ne plus masquer la conversation</string>
<string name="visibility_unlisted">Public calme</string>
<string name="filtered_notifications">Notifications filtrées</string>
<string name="notification_filter_following">Personnes que vous ne suivez pas</string>
<string name="notification_filter_following_explanation">Jusqu\'à ce que vous les approuviez manuellement</string>
<string name="notification_filter_followers">Personnes qui ne vous suivent pas</string>
<string name="notification_filter_new_accounts">Nouveaux comptes</string>
<string name="notification_filter_new_accounts_explanation">Créé au cours des 30 derniers jours</string>
<string name="notification_filter_mentions">Mentions privées non sollicitées</string>
<string name="allow_notifications">Autoriser les notifications</string>
<string name="notifications_from_user">Notifications de %s</string>
<string name="visibility_subtitle_public">Tout le monde sur et en dehors de Mastodon</string>
<string name="visibility_subtitle_unlisted">Moins de fanfares algorithmiques</string>
</resources>

View File

@ -689,7 +689,10 @@ Quanto plus personas tu seque, tanto plus active e interessante illo sera.</stri
<string name="handle_server_explanation_own">Tu casa digital, ubi vive tote tu messages. Non te place? Cambia de servitor a omne momento e porta tu sequitores con te.</string>
<string name="handle_explanation_own">Perque tu pseudonymo indica qui tu es e ubi tu te trova, le gente pote interager con te desde tote le rete social de &lt;a&gt;platteformas basate sur ActivityPub&lt;/a&gt;.</string>
<string name="what_is_activitypub_title">Ques ActivityPub?</string>
<string name="what_is_activitypub">ActivityPub es como le lingua que Mastodon usa con altere retes social.\n\nIllo te permitte de connecter te e interager con le personas non solo sur Mastodon, ma alsi inter apps social differente.</string>
<string name="handle_copied">Manico copiate al area de transferentia.</string>
<string name="qr_code">Codice QR</string>
<string name="scan_qr_code">Scander codice QR</string>
<!-- Shown on a button that saves a file, after it was successfully saved -->
<string name="saved">Salvate</string>
<string name="image_saved">Imagine salvate.</string>
@ -728,8 +731,23 @@ Quanto plus personas tu seque, tanto plus active e interessante illo sera.</stri
<string name="notification_filter_mentions_explanation">Filtrate salvo que illes es in responsa a tu proprie mention o si tu seque le expeditor</string>
<string name="allow_notifications">Permitter le notificationes</string>
<string name="mute_notifications">Dimitter petition de aviso</string>
<plurals name="x_people_you_may_know">
<item quantity="one">%,d persona que tu pote cognoscer</item>
<item quantity="other">%,d personas que tu pote cognoscer</item>
</plurals>
<string name="notifications_from_user">Notificationes de %s</string>
<string name="notifications_muted">Le notificationes de %s ha essite dimittite.</string>
<string name="notifications_allowed">%s apparera ora in tu lista del avisos.</string>
<string name="visibility_subtitle_public">Totes sur e foras de Mastodon</string>
<string name="visibility_subtitle_unlisted">Minus fanfares algorithmic</string>
<string name="visibility_subtitle_followers">Solmente tu sequaces</string>
<string name="visibility_subtitle_private">Tote le personas mentionate in le message</string>
<string name="view_boosts">Vider promovitos</string>
<string name="view_favorites">Vider favoritos</string>
<string name="undo_reblog">Annullar promotion</string>
<string name="undo_favorite">Annullar preferentia</string>
<string name="could_not_reach_server">Impossibile contactar le servitor. Controla tu connexion e retenta?</string>
<string name="connection_timed_out">Le petition exiva del tempore limite. Verifica tu connexion e retenta?</string>
<string name="server_error">Alco errate eveniva parlante con tu servitor. Illo probabilemente non es tu culpa. Retenta?</string>
<string name="not_found">Illo poterea haber essite delite, o forsan illo jammais existeva del toto.</string>
</resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="app_name" translatable="false">Mastodon</string>
<string name="log_in">Log in</string>
@ -11,7 +11,7 @@
<string name="preparing_auth">Preparing for authentication…</string>
<string name="finishing_auth">Finishing authentication…</string>
<string name="user_boosted">%s boosted</string>
<string name="in_reply_to">In reply to %s</string>
<string name="in_reply_to">in reply to %s</string>
<string name="notifications">Notifications</string>
<string name="user_followed_you">%s followed you</string>
@ -171,8 +171,8 @@
<string name="save">Save</string>
<string name="add_alt_text">Add alt text</string>
<string name="visibility_public">Public</string>
<string name="visibility_followers_only">Followers only</string>
<string name="visibility_private">Only people mentioned</string>
<string name="visibility_followers_only">Followers</string>
<string name="visibility_private">Specific people</string>
<string name="recent_searches">Recents</string>
<string name="skip">Skip</string>
<string name="notification_type_follow">New followers</string>
@ -720,4 +720,43 @@
<string name="unpin_post">Unpin from profile</string>
<string name="post_pinned">Post has been pinned</string>
<string name="post_unpinned">Post has been unpinned</string>
<!-- %s is the username -->
<string name="enable_new_post_notifications">Notify me when %s posts</string>
<string name="disable_new_post_notifications">Stop notifying me when %s posts</string>
<string name="new_post_notifications_enabled">Youll get notifications for new posts.</string>
<string name="new_post_notifications_disabled">Youll no longer get notifications for new posts.</string>
<string name="mute_conversation">Mute conversation</string>
<string name="unmute_conversation">Unmute conversation</string>
<string name="visibility_unlisted">Quiet public</string>
<string name="filtered_notifications">Filtered notifications</string>
<string name="filter_notifications">Filter out notifications from...</string>
<string name="notification_filter_following">People you dont follow</string>
<string name="notification_filter_following_explanation">Until you manually approve them</string>
<string name="notification_filter_followers">People not following you</string>
<string name="notification_filter_followers_explanation">Including people who have been following you fewer than 3 days</string>
<string name="notification_filter_new_accounts">New accounts</string>
<string name="notification_filter_new_accounts_explanation">Created within the past 30 days</string>
<string name="notification_filter_mentions">Unsolicited private mentions</string>
<string name="notification_filter_mentions_explanation">Filtered unless its in reply to your own mention or if you follow the sender</string>
<string name="allow_notifications">Allow notifications</string>
<string name="mute_notifications">Dismiss notification request</string>
<plurals name="x_people_you_may_know">
<item quantity="one">%,d person you may know</item>
<item quantity="other">%,d people you may know</item>
</plurals>
<string name="notifications_from_user">Notifications from %s</string>
<string name="notifications_muted">Notifications from %s have been dismissed.</string>
<string name="notifications_allowed">%s will now appear in your notification list.</string>
<string name="visibility_subtitle_public">Everyone on and off Mastodon</string>
<string name="visibility_subtitle_unlisted">Fewer algorithmic fanfares</string>
<string name="visibility_subtitle_followers">Only your followers</string>
<string name="visibility_subtitle_private">Everyone mentioned in the post</string>
<string name="view_boosts">View boosts</string>
<string name="view_favorites">View favorites</string>
<string name="undo_reblog">Undo boost</string>
<string name="undo_favorite">Undo favorite</string>
<string name="could_not_reach_server">Couldnt reach the server. Check your connection and try again?</string>
<string name="connection_timed_out">The request timed out. Check your connection and try again?</string>
<string name="server_error">Something went wrong talking with your server. Its probably not your fault. Try again?</string>
<string name="not_found">It couldve been deleted, or maybe it never existed at all.</string>
</resources>

View File

@ -211,6 +211,7 @@
<item name="android:popupBackground">@drawable/bg_popup</item>
<item name="android:background">@drawable/bg_spinner</item>
<item name="android:backgroundTint">?colorM3OnSurface</item>
<item name="android:popupElevation">3dp</item>
</style>
<style name="Theme.Mastodon.Dialog.Alert" parent="android:Theme.Material.Light.Dialog.Alert">
@ -253,6 +254,7 @@
<style name="Widget.Mastodon.PopupMenu" parent="android:Widget.Material.Light.PopupMenu">
<item name="android:popupBackground">@drawable/bg_popup</item>
<item name="android:popupElevation">3dp</item>
</style>
<style name="Widget.Mastodon.M3.Button" parent="android:Widget.Material.Button">
@ -314,6 +316,7 @@
<style name="Widget.Mastodon.M3.Button.Outlined">
<item name="android:background">@drawable/bg_button_m3_outlined</item>
<item name="android:textColor">@color/button_text_m3_text</item>
<item name="android:tint">@color/action_bar_icons</item>
<item name="android:paddingLeft">24dp</item>
<item name="android:paddingRight">24dp</item>
</style>

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar-SA"/>
<locale android:name="be-BY"/>
<locale android:name="bn-BD"/>
<locale android:name="bs-BA"/>
<locale android:name="ca-ES"/>
<locale android:name="cs-CZ"/>
<locale android:name="da-DK"/>
<locale android:name="de-DE"/>
<locale android:name="el-GR"/>
<locale android:name="en"/>
<locale android:name="es-ES"/>
<locale android:name="eu-ES"/>
<locale android:name="fa-IR"/>
<locale android:name="fi-FI"/>
<locale android:name="fil-PH"/>
<locale android:name="fr-FR"/>
<locale android:name="ga-IE"/>
<locale android:name="gd-GB"/>
<locale android:name="gl-ES"/>
<locale android:name="hi-IN"/>
<locale android:name="hr-HR"/>
<locale android:name="hu-HU"/>
<locale android:name="hy-AM"/>
<locale android:name="ig-NG"/>
<locale android:name="in-ID"/>
<locale android:name="is-IS"/>
<locale android:name="it-IT"/>
<locale android:name="iw-IL"/>
<locale android:name="ja-JP"/>
<locale android:name="ka-GE"/>
<locale android:name="kab"/>
<locale android:name="ko-KR"/>
<locale android:name="lt-LT"/>
<locale android:name="my-MM"/>
<locale android:name="nl-NL"/>
<locale android:name="no-NO"/>
<locale android:name="oc-FR"/>
<locale android:name="pl-PL"/>
<locale android:name="pt-BR"/>
<locale android:name="pt-PT"/>
<locale android:name="ro-RO"/>
<locale android:name="ru-RU"/>
<locale android:name="si-LK"/>
<locale android:name="sl-SI"/>
<locale android:name="sv-SE"/>
<locale android:name="th-TH"/>
<locale android:name="tr-TR"/>
<locale android:name="uk-UA"/>
<locale android:name="ur-IN"/>
<locale android:name="vi-VN"/>
<locale android:name="zh-CN"/>
<locale android:name="zh-TW"/>
</locale-config>