Compare commits

...

31 Commits

Author SHA1 Message Date
Eugen Rochko 7523c296a6
Merge 78f75d4cc1 into b0c77d42c0 2024-04-26 11:14:31 +00:00
Eugen Rochko 78f75d4cc1 New translations strings.xml (Ukrainian) 2024-04-26 13:14:28 +02: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
85 changed files with 1917 additions and 308 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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -709,6 +709,14 @@
<string name="bookmarked">Додано в закладки</string>
<string name="join_server_x_with_invite">Приєднатися до %s за запрошенням</string>
<string name="expired_invite_link">Прострочене запрошувальне посилання</string>
<string name="expired_clipboard_invite_link_alert">Термін дії скопійованого запрошувального посилання для %1$s вийшов, більше не можна зареєструватись за його допомогою.\n\nВи можете попросити інших про нове посилання, зареєструватися через %2$s, або вибрати інший сервер.</string>
<string name="invalid_invite_link">Недійсне запрошувальне посилання</string>
<string name="invalid_clipboard_invite_link_alert">Скопійоване запрошувальне посилання для %1$s не дійсне, не можна зареєструватись за його допомогою.\n\nВи можете попросити інших про нове посилання, зареєструватися через %2$s, або вибрати інший сервер.</string>
<string name="use_invite_link">Використати запрошувальне посилання</string>
<string name="enter_invite_link">Ввести запрошувальне посилання</string>
<string name="this_invite_is_invalid">Це запрошувальне посилання недійсне.</string>
<string name="this_invite_has_expired">Термін дії цього запрошувального посилання минув.</string>
<string name="invite_link_pasted">Посилання вставлено з вашого буфера обміну.</string>
<!-- Shown on a button that saves a file, after it was successfully saved -->
<!-- %s is the username -->
</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>
@ -314,7 +314,7 @@
<string name="instance_signup_closed">This server does not accept new registrations.</string>
<string name="text_copied">Copied to clipboard</string>
<string name="add_bookmark">Bookmark</string>
<string name="remove_bookmark">Remove bookmark</string>
<string name="remove_bookmark">Remove Bookmark</string>
<string name="bookmarks">Bookmarks</string>
<string name="your_favorites">Your favorites</string>
<string name="login_title">Welcome back</string>
@ -720,4 +720,39 @@
<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>
</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>