merge: update to latest master

This commit is contained in:
FineFindus
2023-01-14 12:51:29 +01:00
196 changed files with 4078 additions and 906 deletions

View File

@@ -53,7 +53,10 @@ public class ExternalShareActivity extends FragmentStackActivity{
Intent intent=getIntent();
StringBuilder builder=new StringBuilder();
String subject = "";
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) builder.append(subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)).append("\n\n");
if (intent.hasExtra(Intent.EXTRA_SUBJECT)) {
subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
if (!subject.isBlank()) builder.append(subject).append("\n\n");
}
if (intent.hasExtra(Intent.EXTRA_TEXT)) builder.append(intent.getStringExtra(Intent.EXTRA_TEXT)).append("\n");
String text=builder.toString();
List<Uri> mediaUris;

View File

@@ -28,13 +28,21 @@ public class GlobalUserPreferences{
public static boolean disableSwipe;
public static boolean disableDividers;
public static boolean voteButtonForSingleChoice;
public static boolean showDifferentiatedPushNoticationIcons;
public static boolean uniformNotificationIcon;
public static boolean enableDeleteNotifications;
public static boolean relocatePublishButton;
public static boolean reduceMotion;
public static boolean keepOnlyLatestNotification;
public static String publishButtonText;
public static ThemePreference theme;
public static ColorPreference color;
private final static Type recentLanguagesType = new TypeToken<Map<String, List<String>>>() {}.getType();
public static Map<String, List<String>> recentLanguages;
private final static Type recentEmojisType = new TypeToken<Map<String, Integer>>() {}.getType();
public static Map<String, Integer> recentEmojis;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
}
@@ -52,16 +60,22 @@ public class GlobalUserPreferences{
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
showDifferentiatedPushNoticationIcons=prefs.getBoolean("showDifferentiatedPushNoticationIcons", false);
uniformNotificationIcon=prefs.getBoolean("uniformNotificationIcon", true);
showFederatedTimeline=prefs.getBoolean("showFederatedTimeline", !BuildConfig.BUILD_TYPE.equals("playRelease"));
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
disableMarquee=prefs.getBoolean("disableMarquee", false);
disableSwipe=prefs.getBoolean("disableSwipe", false);
disableDividers=prefs.getBoolean("disableDividers", true);
relocatePublishButton=prefs.getBoolean("relocatePublishButton", true);
voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true);
enableDeleteNotifications=prefs.getBoolean("enableDeleteNotifications", true);
reduceMotion=prefs.getBoolean("reduceMotion", false);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>());
recentEmojis=fromJson(prefs.getString("recentEmojis", "{}"), recentEmojisType, new HashMap<>());
publishButtonText=prefs.getString("publishButtonText", "");
try {
if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
@@ -89,10 +103,16 @@ public class GlobalUserPreferences{
.putBoolean("disableMarquee", disableMarquee)
.putBoolean("disableSwipe", disableSwipe)
.putBoolean("disableDividers", disableDividers)
.putBoolean("showDifferentiatedPushNoticationIcons", showDifferentiatedPushNoticationIcons)
.putBoolean("relocatePublishButton", relocatePublishButton)
.putBoolean("uniformNotificationIcon", uniformNotificationIcon)
.putBoolean("enableDeleteNotifications", enableDeleteNotifications)
.putBoolean("reduceMotion", reduceMotion)
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.putString("publishButtonText", publishButtonText)
.putInt("theme", theme.ordinal())
.putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages))
.putString("recentEmojis", gson.toJson(recentEmojis))
.apply();
}

View File

@@ -16,7 +16,8 @@ import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.fragments.onboarding.CustomLoginFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
@@ -33,7 +34,7 @@ public class MainActivity extends FragmentStackActivity{
if(savedInstanceState==null){
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomLoginFragment());
showFragmentClearingBackStack(new CustomWelcomeFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session;

View File

@@ -8,9 +8,7 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -40,6 +38,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
public static final int NOTIFICATION_ID=178;
private static int notificationID;
@Override
public void onReceive(Context context, Intent intent){
if(BuildConfig.DEBUG){
@@ -127,23 +127,27 @@ public class PushNotificationReceiver extends BroadcastReceiver{
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
}
Drawable avatar=ImageCache.getInstance(context).get(new UrlImageLoaderRequest(pn.icon, V.dp(50), V.dp(50)));
notificationID = GlobalUserPreferences.keepOnlyLatestNotification ? NOTIFICATION_ID : (int)System.currentTimeMillis();
Intent contentIntent=new Intent(context, MainActivity.class);
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
contentIntent.putExtra("fromNotification", true);
contentIntent.putExtra("accountID", accountID);
contentIntent.putExtra("notificationID", notificationID);
if(notification!=null){
contentIntent.putExtra("notification", Parcels.wrap(notification));
}
builder.setContentTitle(pn.title)
.setContentText(pn.body)
.setStyle(new Notification.BigTextStyle().bigText(pn.body))
.setContentIntent(PendingIntent.getActivity(context, accountID.hashCode() & 0xFFFF, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setContentIntent(PendingIntent.getActivity(context, notificationID, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.setWhen(notification==null ? System.currentTimeMillis() : notification.createdAt.toEpochMilli())
.setShowWhen(true)
.setCategory(Notification.CATEGORY_SOCIAL)
.setAutoCancel(true)
.setColor(context.getColor(R.color.shortcut_icon_background));
if(GlobalUserPreferences.showDifferentiatedPushNoticationIcons){
if(!GlobalUserPreferences.uniformNotificationIcon){
switch (pn.notificationType) {
case FAVORITE -> builder.setSmallIcon(R.drawable.ic_fluent_star_24_filled);
case REBLOG -> builder.setSmallIcon(R.drawable.ic_fluent_arrow_repeat_all_24_filled);
@@ -162,6 +166,6 @@ public class PushNotificationReceiver extends BroadcastReceiver{
if(AccountSessionManager.getInstance().getLoggedInAccounts().size()>1){
builder.setSubText(accountName);
}
nm.notify(accountID, NOTIFICATION_ID, builder.build());
nm.notify(accountID, notificationID, builder.build());
}
}

View File

@@ -12,11 +12,14 @@ import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.time.Instant;
import java.time.LocalDate;
@@ -47,9 +50,22 @@ public class MastodonAPIController{
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
private AccountSession session;
private static List<String> badDomains = new ArrayList<>();
static{
thread.start();
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(
MastodonApp.context.getAssets().open("blocks.tsv")
));
String line;
while ((line = reader.readLine()) != null) {
if (line.isBlank() || line.startsWith("#")) continue;
badDomains.add(line.toLowerCase().trim());
}
} catch (IOException e) {
e.printStackTrace();
}
}
public MastodonAPIController(@Nullable AccountSession session){
@@ -57,8 +73,11 @@ public class MastodonAPIController{
}
public <T> void submitRequest(final MastodonAPIRequest<T> req){
final String host = req.getURL().getHost().toLowerCase();
final boolean isBad = badDomains.stream().anyMatch(host::endsWith);
thread.postRunnable(()->{
try{
if (isBad) throw new IllegalArgumentException();
if(req.canceled)
return;
Request.Builder builder=new Request.Builder()

View File

@@ -162,6 +162,8 @@ public class PushSubscriptionManager{
@Override
public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{
result.serverKey=result.serverKey.replace('/','_');
result.serverKey=result.serverKey.replace('+','-');
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);

View File

@@ -9,6 +9,7 @@ import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import java.util.HashMap;
import java.util.function.Consumer;
@@ -18,12 +19,18 @@ import me.grishka.appkit.api.ErrorResponse;
public class StatusInteractionController{
private final String accountID;
private final boolean updateCounters;
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
public StatusInteractionController(String accountID){
public StatusInteractionController(String accountID, boolean updateCounters) {
this.accountID=accountID;
this.updateCounters=updateCounters;
}
public StatusInteractionController(String accountID){
this(accountID, true);
}
public void setFavorited(Status status, boolean favorited, Consumer<Status> cb){
@@ -41,7 +48,7 @@ public class StatusInteractionController{
runningFavoriteRequests.remove(status.id);
result.favouritesCount = Math.max(0, status.favouritesCount) + (favorited ? 1 : -1);
cb.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -50,16 +57,16 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context);
status.favourited=!favorited;
cb.accept(status);
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningFavoriteRequests.put(status.id, req);
status.favourited=favorited;
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged, Consumer<Status> cb){
public void setReblogged(Status status, boolean reblogged, StatusPrivacy visibility, Consumer<Status> cb){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
@@ -67,7 +74,7 @@ public class StatusInteractionController{
if(current!=null){
current.cancel();
}
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged)
SetStatusReblogged req=(SetStatusReblogged) new SetStatusReblogged(status.id, reblogged, visibility)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status reblog){
@@ -75,7 +82,7 @@ public class StatusInteractionController{
runningReblogRequests.remove(status.id);
result.reblogsCount = Math.max(0, status.reblogsCount) + (reblogged ? 1 : -1);
cb.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -84,13 +91,13 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context);
status.reblogged=!reblogged;
cb.accept(status);
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningReblogRequests.put(status.id, req);
status.reblogged=reblogged;
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
public void setBookmarked(Status status, boolean bookmarked){
@@ -111,7 +118,7 @@ public class StatusInteractionController{
public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id);
cb.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(result));
}
@Override
@@ -120,12 +127,12 @@ public class StatusInteractionController{
error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked;
cb.accept(status);
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
if (updateCounters) E.post(new StatusCountersUpdatedEvent(status));
}
}

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
package org.joinmastodon.android.api.requests.notifications;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import java.util.EnumSet;
import java.util.List;
public class DismissNotification extends MastodonAPIRequest<Object>{
public DismissNotification(String id){
super(HttpMethod.POST, "/notifications/" + (id != null ? id + "/dismiss" : "clear"), Object.class);
setRequestBody(new Object());
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
@@ -9,12 +10,29 @@ import java.util.ArrayList;
import java.util.List;
public class CreateStatus extends MastodonAPIRequest<Status>{
public static final Instant DRAFTS_AFTER_INSTANT = Instant.ofEpochMilli(253370764799999L) /* end of 9998 */;
private static final float draftFactor = 31536000000f /* one year */ / 253370764799999f /* end of 9998 */;
public static Instant getDraftInstant() {
// returns an instant between 9999-01-01 00:00:00 and 9999-12-31 23:59:59
// yes, this is a weird implementation for something that hardly matters
return DRAFTS_AFTER_INSTANT.plusMillis(1 + (long) (System.currentTimeMillis() * draftFactor));
}
public CreateStatus(CreateStatus.Request req, String uuid){
super(HttpMethod.POST, "/statuses", Status.class);
setRequestBody(req);
addHeader("Idempotency-Key", uuid);
}
public static class Scheduled extends MastodonAPIRequest<ScheduledStatus>{
public Scheduled(CreateStatus.Request req, String uuid){
super(HttpMethod.POST, "/statuses", ScheduledStatus.class);
setRequestBody(req);
addHeader("Idempotency-Key", uuid);
}
}
public static class Request{
public String status;
public List<String> mediaIds;

View File

@@ -7,4 +7,10 @@ public class DeleteStatus extends MastodonAPIRequest<Status>{
public DeleteStatus(String id){
super(HttpMethod.DELETE, "/statuses/"+id, Status.class);
}
public static class Scheduled extends MastodonAPIRequest<Object> {
public Scheduled(String id) {
super(HttpMethod.DELETE, "/scheduled_statuses/"+id, Object.class);
}
}
}

View File

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

View File

@@ -2,10 +2,17 @@ package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
public class SetStatusReblogged extends MastodonAPIRequest<Status>{
public SetStatusReblogged(String id, boolean reblogged){
public SetStatusReblogged(String id, boolean reblogged, StatusPrivacy visibility){
super(HttpMethod.POST, "/statuses/"+id+"/"+(reblogged ? "reblog" : "unreblog"), Status.class);
setRequestBody(new Object());
Request req = new Request();
req.visibility = visibility;
setRequestBody(req);
}
public static class Request {
public StatusPrivacy visibility;
}
}

View File

@@ -32,7 +32,7 @@ public class AccountSession{
public Preferences preferences;
public AccountActivationInfo activationInfo;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager;
@@ -52,6 +52,10 @@ public class AccountSession{
return domain+"_"+self.id;
}
public String getFullUsername() {
return "@"+self.username+"@"+domain;
}
public MastodonAPIController getApiController(){
if(apiController==null)
apiController=new MastodonAPIController(this);
@@ -64,6 +68,12 @@ public class AccountSession{
return statusInteractionController;
}
public StatusInteractionController getRemoteStatusInteractionController(){
if(remoteStatusInteractionController==null)
remoteStatusInteractionController=new StatusInteractionController(getID(), false);
return remoteStatusInteractionController;
}
public CacheController getCacheController(){
if(cacheController==null)
cacheController=new CacheController(getID());

View File

@@ -13,8 +13,6 @@ import android.net.Uri;
import android.os.Build;
import android.util.Log;
import com.google.gson.JsonParseException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
@@ -104,12 +102,11 @@ public class AccountSessionManager{
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
instances.put(instance.uri, instance);
updateInstanceInfoV2(instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
updateInstanceEmojis(instance, instance.uri);
updateMoreInstanceInfo(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
@@ -328,10 +325,7 @@ public class AccountSessionManager{
@Override
public void onSuccess(Instance instance){
instances.put(domain, instance);
updateInstanceEmojis(instance, domain);
try {
updateInstanceInfoV2(instance);
} catch (Exception ignored) {}
updateMoreInstanceInfo(instance, domain);
}
@Override
@@ -342,16 +336,18 @@ public class AccountSessionManager{
.execNoAuth(domain);
}
public void updateInstanceInfoV2(Instance instance) {
public void updateMoreInstanceInfo(Instance instance, String domain) {
new GetInstance.V2().setCallback(new Callback<>() {
@Override
public void onSuccess(Instance.V2 v2) {
if (instance != null) instance.v2 = v2;
writeAccountsFile();
updateInstanceEmojis(instance, domain);
}
@Override
public void onError(ErrorResponse errorResponse) {}
public void onError(ErrorResponse errorResponse) {
updateInstanceEmojis(instance, domain);
}
}).execNoAuth(instance.uri);
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ScheduledStatus;
public class ScheduledStatusCreatedEvent {
public final ScheduledStatus scheduledStatus;
public final String accountID;
public ScheduledStatusCreatedEvent(ScheduledStatus scheduledStatus, String accountID){
this.scheduledStatus = scheduledStatus;
this.accountID=accountID;
}
}

View File

@@ -0,0 +1,13 @@
package org.joinmastodon.android.events;
import org.joinmastodon.android.model.ScheduledStatus;
public class ScheduledStatusDeletedEvent{
public final String id;
public final String accountID;
public ScheduledStatusDeletedEvent(String id, String accountID){
this.id=id;
this.accountID=accountID;
}
}

View File

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

View File

@@ -1,12 +1,16 @@
package org.joinmastodon.android.fragments;
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
@@ -29,11 +33,12 @@ import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateFormat;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -55,6 +60,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -67,13 +73,15 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.ProgressListener;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.EditStatus;
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
@@ -85,6 +93,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.ComposeAutocompleteViewController;
@@ -100,6 +109,7 @@ import org.joinmastodon.android.ui.utils.TransferSpeedTracker;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ComposeEditText;
import org.joinmastodon.android.ui.views.ComposeMediaLayout;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import org.joinmastodon.android.utils.MastodonLanguage;
@@ -109,6 +119,12 @@ import org.parceler.Parcels;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
@@ -131,6 +147,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private static final int MEDIA_RESULT=717;
private static final int IMAGE_DESCRIPTION_RESULT=363;
private static final int SCHEDULED_STATUS_OPENED_RESULT=161;
private static final int MAX_ATTACHMENTS=4;
private static final String TAG="ComposeFragment";
@@ -154,9 +171,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private String accountID;
private int charCount, charLimit, trimmedCharCount;
private Button publishButton, languageButton;
private PopupMenu languagePopup, visibilityPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
private Button publishButton, languageButton, scheduleTimeBtn, draftsBtn;
private PopupMenu languagePopup, visibilityPopup, draftOptionsPopup;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn, scheduleDraftDismiss;
private ImageView sensitiveIcon;
private ComposeMediaLayout attachmentsView;
private TextView replyText;
@@ -165,8 +182,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private View addPollOptionBtn;
private View sensitiveItem;
private View pollAllowMultipleItem;
private View scheduleDraftView;
private ScrollView scrollView;
private boolean initiallyScrolled = false;
private TextView scheduleDraftText;
private CheckBox pollAllowMultipleCheckbox;
private TextView pollDurationView;
private MenuItem draftMenuItem, undraftMenuItem, scheduleMenuItem, unscheduleMenuItem;
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
@@ -182,6 +204,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private EditText spoilerEdit;
private boolean hasSpoiler;
private boolean sensitive;
private Instant scheduledAt = null;
private ProgressBar sendProgress;
private ImageView sendError;
private View sendingOverlay;
@@ -194,6 +217,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private boolean attachmentsErrorShowing;
private Status editingStatus;
private ScheduledStatus scheduledStatus;
private boolean redraftStatus;
private boolean pollChanged;
private boolean creatingView;
@@ -219,9 +243,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
languageResolver=new MastodonLanguage.LanguageResolver(instance);
redraftStatus=getArguments().getBoolean("redraftStatus", false);
if(getArguments().containsKey("editStatus")){
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
redraftStatus=getArguments().getBoolean("redraftStatus");
}
if(instance==null){
Nav.finish(this);
@@ -231,14 +255,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
}
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
if (bundle.containsKey("scheduledAt")) scheduledAt=(Instant) bundle.getSerializable("scheduledAt");
if(instance.maxTootChars>0)
charLimit=instance.maxTootChars;
else if(instance.configuration!=null && instance.configuration.statuses!=null && instance.configuration.statuses.maxCharacters>0)
charLimit=instance.configuration.statuses.maxCharacters;
else
charLimit=500;
if (editingStatus == null) loadDefaultStatusVisibility(savedInstanceState);
}
@Override
@@ -270,14 +296,30 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiKeyboard.setListener(this::onCustomEmojiClick);
View view=inflater.inflate(R.layout.fragment_compose, container, false);
if(GlobalUserPreferences.relocatePublishButton){
publishButton=view.findViewById(R.id.publish);
// publishButton.setText(editingStatus==null || redraftStatus ? R.string.publish : R.string.save);
publishButton.setEllipsize(TextUtils.TruncateAt.END);
publishButton.setOnClickListener(this::onPublishClick);
publishButton.setSingleLine(true);
publishButton.setVisibility(View.VISIBLE);
draftsBtn=view.findViewById(R.id.drafts_btn);
draftsBtn.setVisibility(View.VISIBLE);
} else {
charCounter=view.findViewById(R.id.char_counter);
charCounter.setVisibility(View.VISIBLE);
charCounter.setText(String.valueOf(charLimit));
}
mainEditText=view.findViewById(R.id.toot_text);
mainEditTextWrap=view.findViewById(R.id.toot_text_wrap);
charCounter=view.findViewById(R.id.char_counter);
charCounter.setText(String.valueOf(charLimit));
scrollView=view.findViewById(R.id.scroll_view);
selfName=view.findViewById(R.id.name);
selfUsername=view.findViewById(R.id.username);
selfAvatar=view.findViewById(R.id.avatar);
selfName=view.findViewById(R.id.self_name);
selfUsername=view.findViewById(R.id.self_username);
selfAvatar=view.findViewById(R.id.self_avatar);
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
selfUsername.setText('@'+self.username+'@'+instanceDomain);
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
@@ -295,6 +337,10 @@ 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);
scheduleDraftView=view.findViewById(R.id.schedule_draft_view);
scheduleDraftText=view.findViewById(R.id.schedule_draft_text);
scheduleDraftDismiss=view.findViewById(R.id.schedule_draft_dismiss);
scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn);
sensitiveIcon=view.findViewById(R.id.sensitive_icon);
sensitiveItem=view.findViewById(R.id.sensitive_item);
replyText=view.findViewById(R.id.reply_text);
@@ -306,6 +352,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
buildVisibilityPopup(visibilityBtn);
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
scheduleDraftDismiss.setOnClickListener(v->updateScheduledAt(null));
scheduleTimeBtn.setOnClickListener(v->pickScheduledDateTime());
sensitiveItem.setOnClickListener(v->toggleSensitive());
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
@@ -355,8 +405,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
DraftPollOption opt=createDraftPollOption();
opt.edit.setText(eopt.title);
}
pollDuration=(int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond();
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), editingStatus.poll.expiresAt);
pollDuration=scheduledStatus == null
? (int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond()
: Integer.parseInt(scheduledStatus.params.poll.expiresIn);
pollDurationStr=UiUtils.formatTimeLeft(getActivity(), scheduledStatus == null
? editingStatus.poll.expiresAt
: Instant.now().plus(pollDuration, ChronoUnit.SECONDS));
updatePollOptionHints();
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr));
}else{
@@ -395,9 +449,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(editingStatus!=null && editingStatus.visibility!=null) {
statusVisibility=editingStatus.visibility;
} else {
loadDefaultStatusVisibility(savedInstanceState);
}
loadDefaultStatusVisibility(savedInstanceState);
updateVisibilityIcon();
visibilityPopup.getMenu().findItem(switch(statusVisibility){
case PUBLIC -> R.id.vis_public;
@@ -441,6 +496,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
outState.putParcelableArrayList("attachments", serializedAttachments);
}
outState.putSerializable("visibility", statusVisibility);
if (scheduledAt != null) outState.putSerializable("scheduledAt", scheduledAt);
if (scheduledStatus != null) outState.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
}
@Override
@@ -523,32 +580,84 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
});
spoilerEdit.addTextChangedListener(new SimpleTextWatcher(e->updateCharCounter()));
if(replyTo!=null){
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
int visibilityNameRes = switch (replyTo.visibility) {
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
};
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_at_symbol;
View replyWrap = view.findViewById(R.id.reply_wrap);
scrollView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
int scrollHeight = scrollView.getHeight();
if (replyWrap.getMinimumHeight() != scrollHeight) {
replyWrap.setMinimumHeight(scrollHeight);
if (!initiallyScrolled) {
initiallyScrolled = true;
scrollView.post(() -> {
int bottom = scrollView.getChildAt(0).getBottom();
int delta = bottom - (scrollView.getScrollY() + scrollView.getHeight());
int space = GlobalUserPreferences.reduceMotion ? 0 : Math.min(V.dp(70), delta);
scrollView.scrollBy(0, delta - space);
if (!GlobalUserPreferences.reduceMotion) {
scrollView.postDelayed(() -> scrollView.smoothScrollBy(0, space), 130);
}
});
}
}
});
visibilityIcon.setBounds(0, 0, V.dp(20), V.dp(20));
Drawable replyArrow = getActivity().getDrawable(R.drawable.ic_fluent_arrow_reply_20_filled);
replyArrow.setBounds(0, 0, V.dp(20), V.dp(20));
replyText.setCompoundDrawables(replyArrow, null, visibilityIcon, null);
replyText.setOnClickListener(v->{
View originalPost = view.findViewById(R.id.original_post);
originalPost.setVisibility(View.VISIBLE);
originalPost.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(replyTo));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ThreadFragment.class, args);
});
ImageView avatar = view.findViewById(R.id.avatar);
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(replyTo.account.avatar));
ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12));
}
};
avatar.setOutlineProvider(roundCornersOutline);
avatar.setClipToOutline(true);
avatar.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(replyTo.account));
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
Nav.go(getActivity(), ProfileFragment.class, args);
});
((TextView) view.findViewById(R.id.name)).setText(replyTo.account.displayName);
((TextView) view.findViewById(R.id.username)).setText(replyTo.account.getDisplayUsername());
view.findViewById(R.id.visibility).setVisibility(View.GONE);
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
});
ImageView moreBtn = view.findViewById(R.id.more);
moreBtn.setImageDrawable(visibilityIcon);
moreBtn.setBackground(null);
TextView timestamp = view.findViewById(R.id.timestamp);
if (replyTo.editedAt==null) timestamp.setText(UiUtils.formatRelativeTimestamp(getContext(), replyTo.createdAt));
else timestamp.setText(getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(getContext(), replyTo.editedAt)));
if (replyTo.spoilerText != null && !replyTo.spoilerText.isBlank()) {
view.findViewById(R.id.spoiler_header).setVisibility(View.VISIBLE);
((TextView) view.findViewById(R.id.spoiler_title_inline)).setText(replyTo.spoilerText);
}
SpannableStringBuilder content = HtmlParser.parse(replyTo.content, replyTo.emojis, replyTo.mentions, replyTo.tags, accountID);
LinkedTextView text = view.findViewById(R.id.text);
if (content.length() > 0) text.setText(content);
else view.findViewById(R.id.display_item_text).setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(16)));
replyText.setText(getString(R.string.in_reply_to, replyTo.account.displayName));
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + UiUtils.getVisibilityText(replyTo));
replyText.setOnClickListener(v->{
scrollView.smoothScrollTo(0, 0);
});
ArrayList<String> mentions=new ArrayList<>();
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!replyTo.account.id.equals(ownID))
@@ -632,42 +741,71 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
publishButton=new Button(getActivity());
publishButton.setText(editingStatus==null || redraftStatus ? R.string.publish : R.string.save);
publishButton.setOnClickListener(this::onPublishClick);
LinearLayout wrap=new LinearLayout(getActivity());
wrap.setOrientation(LinearLayout.HORIZONTAL);
sendProgress=new ProgressBar(getActivity());
LinearLayout.LayoutParams progressLP=new LinearLayout.LayoutParams(V.dp(24), V.dp(24));
progressLP.setMarginEnd(V.dp(16));
progressLP.gravity=Gravity.CENTER_VERTICAL;
wrap.addView(sendProgress, progressLP);
sendError=new ImageView(getActivity());
sendError.setImageResource(R.drawable.ic_fluent_error_circle_24_regular);
sendError.setImageTintList(getResources().getColorStateList(R.color.error_600));
sendError.setScaleType(ImageView.ScaleType.CENTER);
wrap.addView(sendError, progressLP);
sendError.setVisibility(View.GONE);
sendProgress.setVisibility(View.GONE);
LinearLayout.LayoutParams langParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
langParams.setMarginEnd(V.dp(8));
wrap.addView(buildLanguageSelector(), langParams);
wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
wrap.setClipToPadding(false);
MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save);
LinearLayout wrap=new LinearLayout(getActivity());
getActivity().getLayoutInflater().inflate(R.layout.compose_action, wrap);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
updatePublishButtonState();
if(!GlobalUserPreferences.relocatePublishButton){
publishButton = wrap.findViewById(R.id.publish_btn);
publishButton.setOnClickListener(this::onPublishClick);
publishButton.setVisibility(View.VISIBLE);
draftsBtn = wrap.findViewById(R.id.drafts_btn);
draftsBtn.setVisibility(View.VISIBLE);
}else{
charCounter = wrap.findViewById(R.id.char_counter);
charCounter.setVisibility(View.VISIBLE);
charCounter.setText(String.valueOf(charLimit));
}
// draftsBtn = wrap.findViewById(R.id.drafts_btn);
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
draftOptionsPopup.inflate(R.menu.compose_more);
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
draftOptionsPopup.setOnMenuItemClickListener(i->{
int id = i.getItemId();
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
else if (id == R.id.schedule) pickScheduledDateTime();
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
else navigateToUnsentPosts();
return true;
});
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
languageButton = wrap.findViewById(R.id.language_btn);
sendProgress = wrap.findViewById(R.id.send_progress);
sendError = wrap.findViewById(R.id.send_error);
draftsBtn.setOnClickListener(v-> draftOptionsPopup.show());
draftsBtn.setOnTouchListener(draftOptionsPopup.getDragToOpenListener());
updateScheduledAt(scheduledAt != null ? scheduledAt : scheduledStatus != null ? scheduledStatus.scheduledAt : null);
buildLanguageSelector(languageButton);
}
private void navigateToUnsentPosts() {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("hide_fab", true);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
imm.hideSoftInputFromWindow(draftsBtn.getWindowToken(), 0);
if (hasDraft()) {
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
} else {
// result for the previous ScheduledStatusList
setResult(true, null);
// finishing fragment in "onFragmentResult"
Nav.goForResult(getActivity(), ScheduledStatusListFragment.class, args, SCHEDULED_STATUS_OPENED_RESULT, this);
}
}
private void updateLanguage(String lang) {
updateLanguage(languageResolver.from(lang));
updateLanguage(lang == null ? languageResolver.getDefault() : languageResolver.from(lang));
}
private void updateLanguage(MastodonLanguage loc) {
@@ -677,21 +815,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
@SuppressLint("ClickableViewAccessibility")
private Button buildLanguageSelector() {
TypedValue typedValue = new TypedValue();
getActivity().getTheme().resolveAttribute(android.R.attr.textColorSecondary, typedValue, true);
languageButton=new Button(getActivity());
languageButton.setTextColor(typedValue.data);
languageButton.setBackground(getActivity().getDrawable(R.drawable.bg_text_button));
languageButton.setPadding(V.dp(8), 0, V.dp(8), 0);
languageButton.setCompoundDrawablesRelativeWithIntrinsicBounds(getActivity().getDrawable(R.drawable.ic_fluent_local_language_16_regular), null, null, null);
languageButton.setCompoundDrawableTintList(languageButton.getTextColors());
languageButton.setCompoundDrawablePadding(V.dp(6));
private void buildLanguageSelector(Button btn) {
languagePopup=new PopupMenu(getActivity(), languageButton);
languageButton.setOnTouchListener(languagePopup.getDragToOpenListener());
languageButton.setOnClickListener(v->languagePopup.show());
btn.setOnTouchListener(languagePopup.getDragToOpenListener());
btn.setOnClickListener(v->languagePopup.show());
Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
updateLanguage(prefs != null && prefs.postingDefaultLanguage != null && prefs.postingDefaultLanguage.length() > 0
@@ -715,8 +842,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updateLanguage(allLanguages.get(i.getItemId()));
return true;
});
return languageButton;
}
@Override
@@ -753,6 +878,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
updatePublishButtonState();
}
private void resetPublishButtonText() {
int publishText = editingStatus==null || redraftStatus ? R.string.publish : R.string.save;
if(GlobalUserPreferences.relocatePublishButton){
return;
}
if (publishText == R.string.publish && !GlobalUserPreferences.publishButtonText.isEmpty()) {
publishButton.setText(GlobalUserPreferences.publishButtonText);
} else {
publishButton.setText(publishText);
}
}
private void updatePublishButtonState(){
uuid=null;
int nonEmptyPollOptionsCount=0;
@@ -768,6 +905,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
nonDoneAttachmentCount++;
}
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && nonDoneAttachmentCount==0 && (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
sendError.setVisibility(View.GONE);
}
private void onCustomEmojiClick(Emoji emoji){
@@ -783,7 +921,55 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void onPublishClick(View v){
publish();
if (!attachments.isEmpty()
&& statusVisibility != StatusPrivacy.DIRECT
&& !attachments.stream().allMatch(attachment -> attachment.description != null && !attachment.description.isBlank())) {
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_no_image_desc_title)
.setMessage(R.string.sk_no_image_desc)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.publish, (dialog, i)-> publish())
.show();
} else {
publish();
}
}
private void publishErrorCallback(ErrorResponse error) {
wm.removeView(sendingOverlay);
sendingOverlay=null;
sendProgress.setVisibility(View.GONE);
sendError.setVisibility(View.VISIBLE);
publishButton.setEnabled(true);
if (error != null) error.showToast(getActivity());
}
private void createScheduledStatusFinish(ScheduledStatus result) {
wm.removeView(sendingOverlay);
sendingOverlay=null;
Toast.makeText(getContext(), scheduledAt.isAfter(DRAFTS_AFTER_INSTANT) ?
R.string.sk_draft_saved : R.string.sk_post_scheduled, Toast.LENGTH_SHORT).show();
Nav.finish(ComposeFragment.this);
E.post(new ScheduledStatusCreatedEvent(result, accountID));
}
private void maybeDeleteScheduledPost(Runnable callback) {
if (scheduledStatus != null) {
new DeleteStatus.Scheduled(scheduledStatus.id).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
E.post(new ScheduledStatusDeletedEvent(scheduledStatus.id, accountID));
callback.run();
}
@Override
public void onError(ErrorResponse error) {
publishErrorCallback(error);
}
}).exec(accountID);
} else {
callback.run();
}
}
private void publish(){
@@ -793,6 +979,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
req.visibility=statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.scheduledAt = scheduledAt;
if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
}
@@ -829,46 +1016,71 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Callback<Status> resCallback=new Callback<>(){
@Override
public void onSuccess(Status result){
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));
maybeDeleteScheduledPost(() -> {
wm.removeView(sendingOverlay);
sendingOverlay=null;
if(editingStatus==null){
E.post(new StatusCreatedEvent(result, accountID));
if(replyTo!=null){
replyTo.repliesCount++;
E.post(new StatusCountersUpdatedEvent(replyTo));
}
}else{
E.post(new StatusUpdatedEvent(result));
}
}else{
E.post(new StatusUpdatedEvent(result));
}
Nav.finish(ComposeFragment.this);
if (getArguments().getBoolean("navigateToStatus", false)) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
Nav.go(getActivity(), ThreadFragment.class, args);
}
Nav.finish(ComposeFragment.this);
if (getArguments().getBoolean("navigateToStatus", false)) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
if(replyTo!=null) args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo));
Nav.go(getActivity(), ThreadFragment.class, args);
}
});
}
@Override
public void onError(ErrorResponse error){
wm.removeView(sendingOverlay);
sendingOverlay=null;
sendProgress.setVisibility(View.GONE);
sendError.setVisibility(View.VISIBLE);
publishButton.setEnabled(true);
error.showToast(getActivity());
publishErrorCallback(error);
}
};
if(editingStatus!=null && !redraftStatus){
new EditStatus(req, editingStatus.id)
.setCallback(resCallback)
.exec(accountID);
}else{
}else if(req.scheduledAt == null){
new CreateStatus(req, uuid)
.setCallback(resCallback)
.exec(accountID);
}else if(req.scheduledAt.isAfter(Instant.now().plus(10, ChronoUnit.MINUTES))){
// checking for 10 instead of 5 minutes (as per mastodon) because i really don't want
// bugs to occur because the client's clock is wrong by a minute or two - the api
// returns a status instead of a scheduled status if scheduled time is less than 5
// minutes into the future and this is 1. unexpected for the user and 2. hard to handle
new CreateStatus.Scheduled(req, uuid)
.setCallback(new Callback<>() {
@Override
public void onSuccess(ScheduledStatus result) {
maybeDeleteScheduledPost(() -> {
createScheduledStatusFinish(result);
});
}
@Override
public void onError(ErrorResponse error) {
publishErrorCallback(error);
}
}).exec(accountID);
}else{
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_scheduled_too_soon_title)
.setMessage(R.string.sk_scheduled_too_soon)
.setPositiveButton(R.string.ok, (a, b)->{})
.show();
publishErrorCallback(null);
publishButton.setEnabled(false);
}
if (replyTo == null) {
@@ -888,6 +1100,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
List<String> existingMediaIDs=editingStatus.mediaAttachments.stream().map(a->a.id).collect(Collectors.toList());
if(!existingMediaIDs.equals(attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList())))
return true;
if(!statusVisibility.equals(editingStatus.visibility)) return true;
if(scheduledStatus != null && !scheduledStatus.scheduledAt.equals(scheduledAt)) return true;
return pollChanged;
}
boolean pollFieldsHaveContent=false;
@@ -932,14 +1146,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
break;
}
}
} else if (reqCode == SCHEDULED_STATUS_OPENED_RESULT && success && getActivity() != null) {
Nav.finish(this);
}
}
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus==null ? R.string.discard_draft : R.string.discard_changes)
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
.setNegativeButton(R.string.cancel, null)
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
.setPositiveButton(R.string.save, (d, w) -> {
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
publish();
})
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
.show();
}
@@ -1093,7 +1312,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void uploadMediaAttachment(DraftMediaAttachment attachment){
if(areThereAnyUploadingAttachments()){
throw new IllegalStateException("there is already an attachment being uploaded");
throw new IllegalStateException("there is already an attachment being uploaded");
}
attachment.state=AttachmentUploadState.UPLOADING;
attachment.progressBar.setVisibility(View.VISIBLE);
@@ -1118,7 +1337,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override
public void onProgress(long transferred, long total){
if(updateUploadEtaRunnable==null){
UiUtils.runOnUiThread(updateUploadEtaRunnable=ComposeFragment.this::updateUploadETAs, 100);
// getting a NoSuchMethodError: No static method -$$Nest$mupdateUploadETAs(ComposeFragment;)V in class ComposeFragment
// when using method reference out of nowhere after changing code elsewhere. no idea. programming is awful, actually
// noinspection Convert2MethodRef
UiUtils.runOnUiThread(updateUploadEtaRunnable=()->ComposeFragment.this.updateUploadETAs(), 50);
}
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
if(Build.VERSION.SDK_INT>=24)
@@ -1281,7 +1503,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
att.uploadStateText.setText(getString(R.string.file_upload_time_remaining, time));
}
}
UiUtils.runOnUiThread(updateUploadEtaRunnable, 100);
UiUtils.runOnUiThread(updateUploadEtaRunnable, 50);
}
private void onEditMediaDescriptionClick(View v){
@@ -1413,6 +1635,73 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if (attachments.isEmpty()) sensitive = false;
}
private void pickScheduledDateTime() {
LocalDateTime soon = LocalDateTime.now()
.plus(15, ChronoUnit.MINUTES) // so 14:59 doesn't get rounded up to…
.plus(1, ChronoUnit.HOURS) // …15:00, but rather 16:00
.withMinute(0);
new DatePickerDialog(getActivity(), (datePicker, year, arrayMonth, dayOfMonth) -> {
new TimePickerDialog(getActivity(), (timePicker, hour, minute) -> {
updateScheduledAt(LocalDateTime.of(year, arrayMonth + 1, dayOfMonth, hour, minute)
.toInstant(OffsetDateTime.now().getOffset()));
}, soon.getHour(), soon.getMinute(), DateFormat.is24HourFormat(getActivity())).show();
}, soon.getYear(), soon.getMonthValue() - 1, soon.getDayOfMonth()).show();
}
private void updateScheduledAt(Instant scheduledAt) {
this.scheduledAt = scheduledAt;
updatePublishButtonState();
scheduleDraftView.setVisibility(scheduledAt == null ? View.GONE : View.VISIBLE);
draftMenuItem.setVisible(true);
scheduleMenuItem.setVisible(true);
undraftMenuItem.setVisible(false);
unscheduleMenuItem.setVisible(false);
if (scheduledAt != null) {
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
if (scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)) {
draftMenuItem.setVisible(false);
undraftMenuItem.setVisible(true);
scheduleTimeBtn.setVisibility(View.GONE);
scheduleDraftText.setText(R.string.sk_compose_draft);
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_drafts_20_regular, 0, 0, 0);
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_draft));
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(GlobalUserPreferences.relocatePublishButton ? R.drawable.ic_fluent_drafts_24_regular : R.drawable.ic_fluent_drafts_20_filled, 0, 0, 0);
if(GlobalUserPreferences.relocatePublishButton){
publishButton.setCompoundDrawablesWithIntrinsicBounds(scheduledStatus != null && scheduledStatus.scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)
? R.drawable.ic_fluent_save_24_selector : R.drawable.ic_fluent_drafts_24_selector, 0, 0, 0);
}else{
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)
? R.string.save : R.string.sk_draft);
}
} else {
scheduleMenuItem.setVisible(false);
unscheduleMenuItem.setVisible(true);
String at = scheduledAt.atZone(ZoneId.systemDefault()).format(formatter);
scheduleTimeBtn.setVisibility(View.VISIBLE);
scheduleTimeBtn.setText(at);
scheduleDraftText.setText(R.string.sk_compose_scheduled);
scheduleDraftText.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
scheduleDraftDismiss.setContentDescription(getString(R.string.sk_compose_no_schedule));
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(GlobalUserPreferences.relocatePublishButton ? R.drawable.ic_fluent_clock_24_filled : R.drawable.ic_fluent_clock_20_filled, 0, 0, 0);
if(GlobalUserPreferences.relocatePublishButton)
{
publishButton.setCompoundDrawablesWithIntrinsicBounds(scheduledStatus != null && scheduledStatus.scheduledAt.isAfter(DRAFTS_AFTER_INSTANT)
? R.drawable.ic_fluent_save_24_selector : R.drawable.ic_fluent_clock_24_selector, 0, 0, 0);
}else{
publishButton.setText(scheduledStatus != null && scheduledStatus.scheduledAt.equals(scheduledAt)
? R.string.save : R.string.sk_schedule);
}
}
} else {
draftsBtn.setCompoundDrawablesWithIntrinsicBounds(GlobalUserPreferences.relocatePublishButton ? R.drawable.ic_fluent_clock_24_regular : R.drawable.ic_fluent_clock_20_regular, 0, 0, 0);
if(GlobalUserPreferences.relocatePublishButton){
publishButton.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_send_24_selector, 0, 0, 0);
}
resetPublishButtonText();
}
}
private int getMediaAttachmentsCount(){
return attachments.size();
}
@@ -1481,7 +1770,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
case DIRECT -> R.drawable.ic_at_symbol;
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
});
}

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -117,6 +118,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '));
}
private void onFabClick(View v){

View File

@@ -202,6 +202,17 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private void onTabSelected(@IdRes int tab){
Fragment newFragment=fragmentForTab(tab);
if(tab==currentTab){
if(tab == R.id.tab_search){
if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
searchFragment.selectSearch();
return;
}
if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
return;
}
if(tab==currentTab && tab == R.id.tab_search){
if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
return;
@@ -235,6 +246,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
new AccountSwitcherSheet(getActivity()).show();
return true;
}
if(tab==R.id.tab_search){
onTabSelected(R.id.tab_search);
tabBar.selectTab(R.id.tab_search);
searchFragment.selectSearch();
return true;
}
return false;
}

View File

@@ -19,18 +19,22 @@ import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -43,12 +47,9 @@ import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -57,11 +58,14 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class HomeTimelineFragment extends StatusListFragment{
private static final int ANNOUNCEMENTS_RESULT = 654;
private ImageButton fab;
private ImageView toolbarLogo;
private Button toolbarShowNewPostsBtn;
private boolean newPostsBtnShown;
private AnimatorSet currentNewPostsAnim;
private MenuItem announcements;
private String maxID;
@@ -106,6 +110,8 @@ public class HomeTimelineFragment extends StatusListFragment{
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v->UiUtils.pickAccountForCompose(getActivity(), accountID));
updateToolbarLogo();
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
@@ -125,16 +131,40 @@ public class HomeTimelineFragment extends StatusListFragment{
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.home, menu);
announcements = menu.findItem(R.id.announcements);
new GetAnnouncements(false).setCallback(new Callback<>() {
@Override
public void onSuccess(List<Announcement> result) {
boolean hasUnread = result.stream().anyMatch(a -> !a.read);
announcements.setIcon(hasUnread ? R.drawable.ic_announcements_24_badged : R.drawable.ic_fluent_megaphone_24_regular);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getActivity());
}
}).exec(accountID);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
if (item.getItemId() == R.id.settings) Nav.go(getActivity(), SettingsFragment.class, args);
if (item.getItemId() == R.id.announcements) {
Nav.goForResult(getActivity(), AnnouncementsFragment.class, args, ANNOUNCEMENTS_RESULT, this);
}
return true;
}
@Override
public void onFragmentResult(int reqCode, boolean noMoreUnread, Bundle result){
if (reqCode == ANNOUNCEMENTS_RESULT && noMoreUnread) {
announcements.setIcon(R.drawable.ic_fluent_megaphone_24_regular);
}
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
@@ -187,10 +217,8 @@ public class HomeTimelineFragment extends StatusListFragment{
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!filters.isEmpty()){
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
}
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
showNewPostsButton();
@@ -264,18 +292,14 @@ public class HomeTimelineFragment extends StatusListFragment{
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
outer:
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, Filter.FilterContext.HOME);
for(Status s:result){
if(idsBelowGap.contains(s.id))
break;
for(Filter filter:filters){
if(filter.matches(s)){
continue outer;
}
if(filterPredicate.test(s)){
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
targetList.addAll(buildDisplayItems(s));
insertedPosts.add(s);
}
if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none.

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.media.MediaRouter;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
@@ -12,6 +11,7 @@ import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -69,6 +69,7 @@ public class ListTimelineFragment extends StatusListFragment {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID));
}
private void onFabClick(View v){

View File

@@ -74,15 +74,26 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() != R.id.follow_requests) return false;
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
return true;
if (item.getItemId() == R.id.follow_requests) {
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowRequestsListFragment.class, args);
return true;
} else if (item.getItemId() == R.id.clear_notifications) {
UiUtils.confirmDeleteNotification(getActivity(), accountID, null, ()->{
for (int i = 0; i < tabViews.length; i++) {
getFragmentForPage(i).reload();
}
});
return true;
}
return false;
}
@Override

View File

@@ -2,6 +2,8 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -10,7 +12,6 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.Notification;
@@ -78,9 +79,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
case FAVORITE -> getString(R.string.user_favorited);
case POLL -> getString(R.string.poll_ended);
};
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null;
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText, n, null) : null;
if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n);
if(titleItem!=null){
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem imgItem){
@@ -210,7 +211,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
}
private void removeNotification(Notification n){
public void removeNotification(Notification n){
data.remove(n);
preloadedData.remove(n);
int index=-1;

View File

@@ -156,6 +156,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private String getPrefilledText() {
return account == null || AccountSessionManager.getInstance().isSelf(accountID, account)
? null : '@'+account.acct+' ';
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
@@ -178,6 +183,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followingCount=content.findViewById(R.id.following_count);
followingLabel=content.findViewById(R.id.following_label);
followingBtn=content.findViewById(R.id.following_btn);
postsCount=content.findViewById(R.id.posts_count);
postsLabel=content.findViewById(R.id.posts_label);
postsBtn=content.findViewById(R.id.posts_btn);
@@ -267,6 +273,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
cover.setOnClickListener(this::onCoverClick);
refreshLayout.setOnRefreshListener(this);
fab.setOnClickListener(this::onFabClick);
fab.setOnLongClickListener(v->UiUtils.pickAccountForCompose(getActivity(), accountID, getPrefilledText()));
if(loaded){
bindHeaderView();
@@ -279,12 +286,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
followersBtn.setOnClickListener(this::onFollowersOrFollowingClick);
followingBtn.setOnClickListener(this::onFollowersOrFollowingClick);
if (account != null && account.bot) {
username.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_fluent_bot_24_filled, 0, 0, 0);
}
username.setOnLongClickListener(v->{
String username=account.acct;
if(!username.contains("@")){
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
}
UiUtils.copyText(getActivity(), '@'+username);
UiUtils.copyText(username, '@'+usernameString);
return true;
});
@@ -544,17 +555,29 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(relationship==null && !isOwnProfile)
return;
inflater.inflate(isOwnProfile ? R.menu.profile_own : R.menu.profile, menu);
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getDisplayUsername()));
if(isOwnProfile){
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags, R.id.favorites, R.id.scheduled, R.id.share);
}else{
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags, R.id.favorites, R.id.scheduled);
}
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
if(isOwnProfile)
return;
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername()));
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), mute);
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
if(relationship.following) {
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername()));
MenuItem hideBoosts = menu.findItem(R.id.hide_boosts);
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
manageUserLists.setVisible(true);
}else {
menu.findItem(R.id.hide_boosts).setVisible(false);
@@ -623,6 +646,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), FollowedHashtagsFragment.class, args);
}else if(id==R.id.scheduled){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ScheduledStatusListFragment.class, args);
}
return true;
}
@@ -937,9 +964,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
if(!AccountSessionManager.getInstance().isSelf(accountID, account)){
args.putString("prefilledText", '@'+account.acct+' ');
}
if(getPrefilledText() != null) args.putString("prefilledText", getPrefilledText());
Nav.go(getActivity(), ComposeFragment.class, args);
}

View File

@@ -0,0 +1,171 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetScheduledStatuses;
import org.joinmastodon.android.events.ScheduledStatusCreatedEvent;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ScheduledStatusListFragment extends BaseStatusListFragment<ScheduledStatus> {
private String nextMaxID;
private ImageButton fab;
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
public ScheduledStatusListFragment() {
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.sk_unsent_posts);
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
fab.setOnClickListener(v -> Nav.go(getActivity(), ComposeFragment.class, args));
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, args));
if (getArguments().getBoolean("hide_fab", false)) fab.setVisibility(View.GONE);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null);
}
@Override
protected void addAccountToKnown(ScheduledStatus s) {}
@Override
public void onItemClick(String id) {
final Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = getStatusByID(id);
Status status = scheduledStatus.toStatus();
args.putParcelable("scheduledStatus", Parcels.wrap(scheduledStatus));
args.putParcelable("editStatus", Parcels.wrap(status));
args.putString("sourceText", status.text);
args.putString("sourceSpoiler", status.spoilerText);
args.putBoolean("redraftStatus", true);
setResult(true, null);
// closing this scheduled status list if another status list is opened from compose fragment
Nav.goForResult(getActivity(), ComposeFragment.class, args, SCHEDULED_STATUS_LIST_OPENED, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result) {
if (reqCode == SCHEDULED_STATUS_LIST_OPENED && success && getActivity() != null) {
Nav.finish(this);
}
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetScheduledStatuses(offset==0 ? null : nextMaxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(HeaderPaginationList<ScheduledStatus> result){
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountID);
}
// copied from StatusListFragment.java
@Subscribe
public void onScheduledStatusDeleted(ScheduledStatusDeletedEvent ev){
if(!ev.accountID.equals(accountID)) return;
ScheduledStatus status=getStatusByID(ev.id);
if(status==null) return;
removeStatus(status);
}
// copied from StatusListFragment.java
@Subscribe
public void onScheduledStatusCreated(ScheduledStatusCreatedEvent ev){
if(!ev.accountID.equals(accountID)) return;
prependItems(Collections.singletonList(ev.scheduledStatus), true);
scrollToTop();
}
// copied from StatusListFragment.java
protected void removeStatus(ScheduledStatus status){
data.remove(status);
preloadedData.remove(status);
int index=-1;
for(int i=0;i<displayItems.size();i++){
if(status.id.equals(displayItems.get(i).parentID)){
index=i;
break;
}
}
if(index==-1)
return;
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(status.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
// copied from StatusListFragment.java
protected ScheduledStatus getStatusByID(String id){
for(ScheduledStatus s:data){
if(s.id.equals(id)){
return s;
}
}
for(ScheduledStatus s:preloadedData){
if(s.id.equals(id)){
return s;
}
}
return null;
}
}

View File

@@ -9,7 +9,7 @@ import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -19,8 +19,11 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.animation.LinearInterpolator;
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.RadioButton;
@@ -43,12 +46,15 @@ import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.function.Consumer;
@@ -58,6 +64,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
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.imageloader.ImageCache;
@@ -86,6 +94,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
setTitle(R.string.settings);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
String instanceName = UiUtils.getInstanceName(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
@@ -121,8 +131,54 @@ public class SettingsFragment extends MastodonToolbarFragment{
case NORD -> R.string.sk_color_palette_nord;
});
}));
items.add(new ButtonItem(R.string.sk_settings_publish_button_text, R.drawable.ic_fluent_send_24_regular, b-> {
updatePublishText(b);
if (GlobalUserPreferences.relocatePublishButton) {
b.setOnClickListener(l -> {
Toast.makeText(getActivity(), R.string.sk_disable_relocate_publish_button_to_enable_customization,
Toast.LENGTH_LONG).show();
});
} else {
b.setOnClickListener(l -> {
FrameLayout inputWrap = new FrameLayout(getContext());
EditText input = new EditText(getContext());
input.setHint(R.string.publish);
input.setText(GlobalUserPreferences.publishButtonText.trim());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(V.dp(16), V.dp(4), V.dp(16), V.dp(16));
input.setLayoutParams(params);
inputWrap.addView(input);
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(inputWrap)
.setPositiveButton(R.string.save, (d, which) -> {
GlobalUserPreferences.publishButtonText = input.getText().toString().trim();
GlobalUserPreferences.save();
updatePublishText(b);
})
.setNeutralButton(R.string.clear, (d, which) -> {
GlobalUserPreferences.publishButtonText = "";
GlobalUserPreferences.save();
updatePublishText(b);
})
.setNegativeButton(R.string.cancel, (d, which) -> {
})
.show();
});}
}));
items.add(new SwitchItem(R.string.sk_settings_uniform_icon_for_notifications, R.drawable.ic_ntf_logo, GlobalUserPreferences.uniformNotificationIcon, i->{
GlobalUserPreferences.uniformNotificationIcon=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_reduce_motion, R.drawable.ic_fluent_star_emphasis_24_regular, GlobalUserPreferences.reduceMotion, i->{
GlobalUserPreferences.reduceMotion=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_behavior));
items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{
GlobalUserPreferences.showFederatedTimeline=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
GlobalUserPreferences.playGifs=i.checked;
GlobalUserPreferences.save();
@@ -144,12 +200,26 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new SwitchItem(R.string.sk_settings_show_differentiated_notification_icons, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showDifferentiatedPushNoticationIcons, this::onNotificationStyleChanged));
// items.add(new SwitchItem(R.string.sk_settings_show_differentiated_notification_icons, R.drawable.ic_ntf_logo, GlobalUserPreferences.showUniformPushNoticationIcons, this::onNotificationStyleChanged));
items.add(new SwitchItem(R.string.sk_disable_dividers, R.drawable.ic_fluent_timeline_24_regular, GlobalUserPreferences.disableDividers, i->{
GlobalUserPreferences.disableDividers=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
// items.add(new SwitchItem(R.string.sk_enable_delete_notifications, R.drawable.ic_fluent_delete_24_regular, GlobalUserPreferences.enableDeleteNotifications, i->{
// GlobalUserPreferences.enableDeleteNotifications=i.checked;
// GlobalUserPreferences.save();
// needAppRestart=true;
// }));
items.add(new SwitchItem(R.string.sk_relocate_publish_button, R.drawable.ic_fluent_arrow_autofit_down_24_regular, GlobalUserPreferences.relocatePublishButton, i->{
GlobalUserPreferences.relocatePublishButton=i.checked;
GlobalUserPreferences.save();
}));
// items.add(new SwitchItem(R.string.sk_settings_hide_translate_in_timeline, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{
// GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
// GlobalUserPreferences.save();
// needAppRestart=true;
// }));
items.add(new HeaderItem(R.string.home_timeline));
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
@@ -164,11 +234,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.loadNewPosts=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{
GlobalUserPreferences.showFederatedTimeline=i.checked;
GlobalUserPreferences.save();
needAppRestart=true;
}));
items.add(new HeaderItem(R.string.settings_notifications));
items.add(notificationPolicyItem=new NotificationPolicyItem());
@@ -176,30 +241,62 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_alert_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked)));
items.add(new SwitchItem(R.string.sk_keep_only_latest_notification, R.drawable.ic_fluent_custom_alert_latest_24_regular, GlobalUserPreferences.keepOnlyLatestNotification, i->{
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_boring));
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new HeaderItem(R.string.settings_account));
items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_posting, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/preferences/other"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_filters, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/filters"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_auth, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"), R.drawable.ic_fluent_open_24_regular));
items.add(new RedHeaderItem(R.string.settings_spicy));
items.add(new HeaderItem(instanceName));
items.add(new TextItem(R.string.sk_settings_rules, ()->{
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}, R.drawable.ic_fluent_task_list_ltr_24_regular));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
boolean translationAvailable = instance.v2 != null && instance.v2.configuration.translation != null && instance.v2.configuration.translation.enabled;
items.add(new SmallTextItem(getString(translationAvailable ?
R.string.sk_settings_translation_availability_note_available :
R.string.sk_settings_translation_availability_note_unavailable, instance.title)));
items.add(new HeaderItem(R.string.sk_settings_about));
// items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular));
// items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular));
if (GithubSelfUpdater.needSelfUpdating()) {
checkForUpdateItem = new TextItem(R.string.sk_check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates);
items.add(checkForUpdateItem);
}
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/LucasGGamerM/moshidon")));
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/LucasGGamerM/moshidon"), R.drawable.ic_fluent_open_24_regular));
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sponsors/LucasGGamerM"), R.drawable.ic_fluent_heart_24_regular));
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.sk_clear_recent_languages, ()->UiUtils.showConfirmationAlert(getActivity(), R.string.sk_clear_recent_languages, R.string.sk_confirm_clear_recent_languages, R.string.clear, ()->{
GlobalUserPreferences.recentLanguages.remove(accountID);
GlobalUserPreferences.save();
})));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
items.add(new TextItem(R.string.sk_clear_recent_emoji, ()-> {
GlobalUserPreferences.recentEmojis.clear();
GlobalUserPreferences.save();
}));
// items.add(new TextItem(R.string.log_out, this::confirmLogOut));
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
private void updatePublishText(Button btn) {
if (GlobalUserPreferences.publishButtonText.isBlank()) btn.setText(R.string.publish);
else btn.setText(GlobalUserPreferences.publishButtonText);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
@@ -360,7 +457,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
private void onNotificationStyleChanged(SwitchItem item){
GlobalUserPreferences.showDifferentiatedPushNoticationIcons=item.checked;
GlobalUserPreferences.uniformNotificationIcon=item.checked;
GlobalUserPreferences.save();
}
@@ -470,6 +567,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.text=getString(text);
}
public HeaderItem(String text) {
this.text=text;
}
@Override
public int getViewType(){
return 0;
@@ -544,19 +645,42 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
private class SmallTextItem extends Item {
private String text;
public SmallTextItem(String text) {
this.text = text;
}
@Override
public int getViewType() {
return 9;
}
}
private class TextItem extends Item{
private String text;
private Runnable onClick;
private boolean loading;
private int icon;
public TextItem(@StringRes int text, Runnable onClick) {
this(text, onClick, false);
this(text, onClick, false, 0);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading){
public TextItem(@StringRes int text, Runnable onClick, boolean loading) {
this(text, onClick, loading, 0);
}
public TextItem(@StringRes int text, Runnable onClick, @DrawableRes int icon) {
this(text, onClick, false, icon);
}
public TextItem(@StringRes int text, Runnable onClick, boolean loading, @DrawableRes int icon){
this.text=getString(text);
this.onClick=onClick;
this.loading=loading;
this.icon=icon;
}
@Override
@@ -613,6 +737,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
case 6 -> new FooterViewHolder();
case 7 -> new UpdateViewHolder();
case 8 -> new ButtonViewHolder();
case 9 -> new SmallTextViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@@ -809,17 +934,20 @@ public class SettingsFragment extends MastodonToolbarFragment{
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
private final TextView text;
private final ProgressBar progress;
private final ImageView icon;
public TextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text = itemView.findViewById(R.id.text);
progress = itemView.findViewById(R.id.progress);
icon = itemView.findViewById(R.id.icon);
}
@Override
public void onBind(TextItem item){
text.setText(item.text);
progress.animate().alpha(item.loading ? 1 : 0);
if (item.icon != 0) icon.setImageDrawable(getActivity().getTheme().getDrawable(item.icon));
}
@Override
@@ -828,6 +956,26 @@ public class SettingsFragment extends MastodonToolbarFragment{
}
}
private class SmallTextViewHolder extends BindableViewHolder<SmallTextItem> {
private final TextView text;
;
public SmallTextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text = itemView.findViewById(R.id.text);
}
@Override
public void onBind(SmallTextItem item){
text.setText(item.text);
TypedValue val = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.textColorSecondary, val, true);
text.setTextColor(getResources().getColor(val.resourceId, getContext().getTheme()));
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
}
}
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
private final TextView text;
public FooterViewHolder(){

View File

@@ -54,7 +54,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -139,7 +139,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
action=getString(R.string.edit_multiple_changed);
}
}
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null));
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null, null));
}
return items;
}

View File

@@ -29,7 +29,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected EventListener eventListener=new EventListener();
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true);
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null);
}
@Override

View File

@@ -5,7 +5,6 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
@@ -17,6 +16,7 @@ import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.Collections;
@@ -92,16 +92,10 @@ public class ThreadFragment extends StatusListFragment{
}
private List<Status> filterStatuses(List<Status> statuses){
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
if(filters.isEmpty())
return statuses;
return statuses.stream().filter(status->{
for(Filter filter:filters){
if(filter.matches(status))
return false;
}
return true;
}).collect(Collectors.toList());
StatusFilterPredicate statusFilterPredicate=new StatusFilterPredicate(accountID,Filter.FilterContext.THREAD);
return statuses.stream()
.filter(statusFilterPredicate)
.collect(Collectors.toList());
}
@Override

View File

@@ -358,4 +358,10 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
return position;
}
}
public void selectSearch(){
searchEdit.requestFocus();
onSearchEditFocusChanged(searchEdit, true);
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
}
}

View File

@@ -75,7 +75,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true);
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null);
};
}

View File

@@ -36,10 +36,10 @@ import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class CustomLoginFragment extends InstanceCatalogFragment {
public class CustomWelcomeFragment extends InstanceCatalogFragment {
private View headerView;
public CustomLoginFragment() {
public CustomWelcomeFragment() {
super(R.layout.fragment_welcome_custom, 1);
}
@@ -55,9 +55,9 @@ public class CustomLoginFragment extends InstanceCatalogFragment {
dataLoaded();
}
// @Override
@Override
protected void onUpdateToolbar(){
// super.onUpdateToolbar();
super.onUpdateToolbar();
if (!canGoBack()) {
ImageView toolbarLogo=new ImageView(getActivity());
@@ -137,9 +137,11 @@ public class CustomLoginFragment extends InstanceCatalogFragment {
headerView.findViewById(R.id.more).setVisibility(View.GONE);
headerView.findViewById(R.id.visibility).setVisibility(View.GONE);
((TextView) headerView.findViewById(R.id.username)).setText("@moshidon");
headerView.findViewById(R.id.separator).setVisibility(View.GONE);
headerView.findViewById(R.id.timestamp).setVisibility(View.GONE);
headerView.findViewById(R.id.unread_indicator).setVisibility(View.GONE);
((TextView) headerView.findViewById(R.id.username)).setText(R.string.sk_app_username);
((TextView) headerView.findViewById(R.id.name)).setText(R.string.sk_app_name);
((TextView) headerView.findViewById(R.id.timestamp)).setText(R.string.time_now);
((ImageView) headerView.findViewById(R.id.avatar)).setImageDrawable(getActivity().getDrawable(R.mipmap.ic_launcher));
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(this);
@@ -168,7 +170,7 @@ public class CustomLoginFragment extends InstanceCatalogFragment {
return mergeAdapter;
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder> {
public InstancesAdapter(){
super(imgLoader);
}
@@ -204,11 +206,6 @@ public class CustomLoginFragment extends InstanceCatalogFragment {
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_custom, list);
// itemView.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(16));
// TypedValue value = new TypedValue();
// getActivity().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, value, true);
// itemView.setBackground(getActivity().getTheme().getDrawable(R.drawable.bg_search_field));
title=findViewById(R.id.title);
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -65,7 +66,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
@@ -77,8 +78,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
// setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
// view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
// @Override

View File

@@ -237,7 +237,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null);
for(StatusDisplayItem item:items){
if(item instanceof ImageStatusDisplayItem isdi){
isdi.horizontalInset=V.dp(40+32);

View File

@@ -164,6 +164,10 @@ public class Account extends BaseModel{
return '@'+acct;
}
public String getShortUsername() {
return '@'+acct.split("@")[0];
}
@Override
public String toString(){
return "Account{"+

View File

@@ -0,0 +1,63 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
@Parcel
public class Announcement extends BaseModel implements DisplayItemsParent {
@RequiredField
public String id;
@RequiredField
public String content;
public Instant startsAt;
public Instant endsAt;
public boolean published;
public boolean allDay;
public Instant publishedAt;
public Instant updatedAt;
public boolean read;
public List<Emoji> emojis;
public List<Mention> mentions;
public List<Hashtag> tags;
@Override
public String toString() {
return "Announcement{" +
"id='" + id + '\'' +
", content='" + content + '\'' +
", startsAt=" + startsAt +
", endsAt=" + endsAt +
", published=" + published +
", allDay=" + allDay +
", publishedAt=" + publishedAt +
", updatedAt=" + updatedAt +
", read=" + read +
", emojis=" + emojis +
", mentions=" + mentions +
", tags=" + tags +
'}';
}
public Status toStatus() {
Status s = new Status();
s.id = id;
s.mediaAttachments = List.of();
s.createdAt = startsAt != null ? startsAt : publishedAt;
if (updatedAt != null) s.editedAt = updatedAt;
s.content = s.text = content;
s.spoilerText = "";
s.visibility = StatusPrivacy.PUBLIC;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
return s;
}
@Override
public String getID() {
return id;
}
}

View File

@@ -6,12 +6,14 @@ import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;
@Parcel
public class Filter extends BaseModel{
@RequiredField
public String id;
@@ -21,6 +23,7 @@ public class Filter extends BaseModel{
public Instant expiresAt;
public boolean irreversible;
public boolean wholeWord;
public FilterAction filterAction;
@SerializedName("context")
private List<FilterContext> _context;
@@ -76,4 +79,11 @@ public class Filter extends BaseModel{
@SerializedName("thread")
THREAD
}
public enum FilterAction{
@SerializedName("hide")
HIDE,
@SerializedName("warn")
WARN
}
}

View File

@@ -0,0 +1,8 @@
package org.joinmastodon.android.model;
import org.parceler.Parcel;
@Parcel
public class FilterResult extends BaseModel {
public Filter filter;
}

View File

@@ -57,6 +57,11 @@ public class Poll extends BaseModel{
public String title;
public Integer votesCount;
public Option() {}
public Option(String title) {
this.title = title;
}
@Override
public String toString(){
return "Option{"+

View File

@@ -0,0 +1,79 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.Poll.Option;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
@Parcel
public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
@RequiredField
public String id;
@RequiredField
public Instant scheduledAt;
@RequiredField
public Params params;
@RequiredField
public List<Attachment> mediaAttachments;
@Override
public String getID() {
return id;
}
@Parcel
public static class Params {
@RequiredField
public String text;
public String spoilerText;
@RequiredField
public StatusPrivacy visibility;
public long inReplyToId;
public ScheduledPoll poll;
public boolean sensitive;
public boolean withRateLimit;
public String language;
public String idempotency;
public String applicationId;
public List<String> mediaIds;
}
@Parcel
public static class ScheduledPoll {
@RequiredField
public String expiresIn;
@RequiredField
public List<String> options;
public boolean multiple;
public boolean hideTotals;
public Poll toPoll() {
Poll p = new Poll();
p.voted = true;
p.emojis = List.of();
p.ownVotes = List.of();
p.multiple = multiple;
p.options = options.stream().map(Option::new).collect(Collectors.toList());
return p;
}
}
public Status toStatus() {
Status s = new Status();
s.id = id;
s.mediaAttachments = mediaAttachments;
s.createdAt = scheduledAt;
s.content = s.text = params.text;
s.spoilerText = params.spoilerText;
s.visibility = params.visibility;
s.language = params.language;
s.mentions = List.of();
s.tags = List.of();
s.emojis = List.of();
if (params.poll != null) s.poll = params.poll.toPoll();
return s;
}
}

View File

@@ -20,7 +20,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
public Instant createdAt;
@RequiredField
public Account account;
// @RequiredField
// @RequiredField
public String content;
@RequiredField
public StatusPrivacy visibility;
@@ -40,7 +40,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
public long favouritesCount;
public long repliesCount;
public Instant editedAt;
public boolean wantsTranslation;
public List<FilterResult> filtered;
public String url;
public String inReplyToId;
@@ -50,7 +50,6 @@ public class Status extends BaseModel implements DisplayItemsParent{
public Card card;
public String language;
public String text;
public String translation;
public boolean favourited;
public boolean reblogged;

View File

@@ -23,7 +23,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.onboarding.CustomLoginFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
@@ -77,7 +78,7 @@ public class AccountSwitcherSheet extends BottomSheet{
holder.avatar.setImageResource(R.drawable.ic_fluent_add_circle_24_filled);
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary)));
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, ()->{
Nav.go(activity, CustomLoginFragment.class, null);
Nav.go(activity, CustomWelcomeFragment.class, null);
dismiss();
}));

View File

@@ -1,8 +1,9 @@
package org.joinmastodon.android.ui;
import static org.joinmastodon.android.GlobalUserPreferences.recentEmojis;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
@@ -12,8 +13,13 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.EmojiUpdatedEvent;
@@ -21,13 +27,13 @@ import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
@@ -40,6 +46,9 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class CustomEmojiPopupKeyboard extends PopupKeyboard{
//determines how many emoji need to be clicked, before it disappears from the recent emojis
private static final int NEW_RECENT_VALUE=15;
private List<EmojiCategory> emojis;
private UsableRecyclerView list;
private ListImageLoaderWrapper imgLoader;
@@ -82,6 +91,17 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
list.setLayoutManager(lm);
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
// inject category with last used emojis
if (!recentEmojis.isEmpty()) {
List<Emoji> allAvailableEmojis = emojis.stream().flatMap(category -> category.emojis.stream()).collect(Collectors.toList());
List<Emoji> recentEmojiList = new ArrayList<>();
for (String emojiCode : recentEmojis.keySet().stream().sorted(Comparator.comparingInt(GlobalUserPreferences.recentEmojis::get).reversed()).collect(Collectors.toList())) {
Optional<Emoji> element = allAvailableEmojis.stream().filter(e -> e.shortcode.equals(emojiCode)).findFirst();
element.ifPresent(recentEmojiList::add);
}
emojis.add(0, new EmojiCategory(activity.getString(R.string.sk_emoji_recent), recentEmojiList));
}
for(EmojiCategory category:emojis)
adapter.addAdapter(new SingleCategoryAdapter(category));
list.setAdapter(adapter);
@@ -100,6 +120,11 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
list.setBackgroundColor(UiUtils.getThemeColor(activity, android.R.attr.colorBackground));
list.setSelector(null);
//remove recently used afterwards, it would get duplicated otherwise
if (!recentEmojis.isEmpty()) {
emojis.remove(0);
}
return list;
}
@@ -107,6 +132,19 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
this.listener=listener;
}
private void increaseEmojiCount(Emoji emoji) {
Integer usageCount = recentEmojis.get(emoji.shortcode);
if (usageCount != null) {
recentEmojis.put(emoji.shortcode, usageCount + 1);
} else {
recentEmojis.put(emoji.shortcode, NEW_RECENT_VALUE);
}
recentEmojis.entrySet().removeIf(e -> e.getValue() <= 0);
recentEmojis.replaceAll((k, v) -> v - 1);
GlobalUserPreferences.save();
}
@SuppressLint("NotifyDataSetChanged")
@Subscribe
public void onEmojiUpdated(EmojiUpdatedEvent ev){
@@ -203,6 +241,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
@Override
public void onClick(){
increaseEmojiCount(item);
listener.accept(item);
}
}

View File

@@ -32,7 +32,7 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
View title=alert.findViewById(titleID);
if(title!=null){
int pad=V.dp(24);
title.setPadding(pad, pad, pad, pad);
title.setPadding(pad, pad, pad, V.dp(18));
}
}
int titleDividerID=getContext().getResources().getIdentifier("titleDividerNoCustom", "id", "android");

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.os.Build;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
@@ -9,6 +10,7 @@ import android.widget.SeekBar;
import android.widget.TextView;
import org.joinmastodon.android.AudioPlayerService;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
@@ -126,6 +128,10 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
lastKnownPositionTime=SystemClock.uptimeMillis();
this.playing=playing;
playPauseBtn.setImageResource(playing ? R.drawable.ic_fluent_pause_circle_24_filled : R.drawable.ic_fluent_play_circle_24_filled);
playPauseBtn.setContentDescription(MastodonApp.context.getResources().getString(playing ? R.string.pause : R.string.play));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
playPauseBtn.setTooltipText(playPauseBtn.getContentDescription());
}
if(!playing){
lastRemainingSeconds=-1;
time.setText(formatDuration((int) item.attachment.getDuration()));

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@@ -98,8 +99,13 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
case DIRECT -> R.drawable.ic_at_symbol;
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
});
visibility.setContentDescription(UiUtils.getVisibilityText(s));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
visibility.setTooltipText(visibility.getContentDescription());
}
}
@Override

View File

@@ -1,9 +1,13 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@@ -11,6 +15,9 @@ import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.BounceInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
@@ -18,14 +25,18 @@ import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.function.Consumer;
import me.grishka.appkit.Nav;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
@@ -50,9 +61,18 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
private final TextView reply, boost, favorite, bookmark;
private final ImageView share;
private static final Animation opacityOut, opacityIn;
private static AnimationSet animSet;
private View touchingView = null;
private final Runnable longClickRunnable = () -> { if (touchingView != null) touchingView.performLongClick(); };
private boolean longClickPerformed = false;
private final Runnable longClickRunnable = () -> {
longClickPerformed = touchingView != null && touchingView.performLongClick();
if (longClickPerformed && touchingView != null) {
touchingView.startAnimation(opacityIn);
touchingView.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
}
};
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@Override
@@ -64,13 +84,22 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
};
static {
opacityOut = new AlphaAnimation(1, 0.7f);
opacityOut.setDuration(200);
opacityOut = new AlphaAnimation(1, 0.55f);
opacityOut.setDuration(300);
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
opacityOut.setFillAfter(true);
opacityIn = new AlphaAnimation(0.7f, 1);
opacityIn.setDuration(300);
opacityIn = new AlphaAnimation(0.55f, 1);
opacityIn.setDuration(400);
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
Animation spin = new RotateAnimation(0, 360,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
animSet = new AnimationSet(true);
animSet.setInterpolator(CubicBezierInterpolator.DEFAULT);
animSet.addAnimation(spin);
animSet.addAnimation(opacityIn);
animSet.setDuration(400);
}
public Holder(Activity activity, ViewGroup parent){
@@ -93,6 +122,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
View bookmark=findViewById(R.id.bookmark_btn);
reply.setOnTouchListener(this::onButtonTouch);
reply.setOnClickListener(this::onReplyClick);
reply.setOnLongClickListener(this::onReplyLongClick);
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
boost.setOnTouchListener(this::onButtonTouch);
boost.setOnClickListener(this::onBoostClick);
@@ -100,9 +130,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
favorite.setOnTouchListener(this::onButtonTouch);
favorite.setOnClickListener(this::onFavoriteClick);
favorite.setOnLongClickListener(this::onFavoriteLongClick);
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
bookmark.setOnTouchListener(this::onButtonTouch);
bookmark.setOnClickListener(this::onBookmarkClick);
bookmark.setOnLongClickListener(this::onBookmarkLongClick);
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
share.setOnTouchListener(this::onButtonTouch);
share.setOnClickListener(this::onShareClick);
@@ -115,6 +147,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(reply, item.status.repliesCount);
bindButton(boost, item.status.reblogsCount);
bindButton(favorite, item.status.favouritesCount);
reply.setSelected(item.status.repliesCount > 0);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
@@ -136,15 +169,15 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
int action = event.getAction();
long eventDuration = event.getEventTime() - event.getDownTime();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
touchingView = null;
v.removeCallbacks(longClickRunnable);
v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
if (!longClickPerformed) v.animate().scaleX(1).scaleY(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
if (disabled) return true;
if (action == MotionEvent.ACTION_UP && eventDuration < ViewConfiguration.getLongPressTimeout()) v.performClick();
else v.startAnimation(opacityIn);
if (action == MotionEvent.ACTION_UP && !longClickPerformed) v.performClick();
else if (!longClickPerformed) v.startAnimation(opacityIn);
} else if (action == MotionEvent.ACTION_DOWN) {
longClickPerformed = false;
touchingView = v;
// 20dp to center in middle of icon, because: (icon width = 24dp) / 2 + (paddingStart = 8dp)
v.setPivotX(V.dp(20));
@@ -164,41 +197,167 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}
private boolean onReplyLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
Bundle args=new Bundle();
String accountID = session.getID();
args.putString("account", accountID);
UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> {
args.putParcelable("replyTo", Parcels.wrap(status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
}, null);
return true;
}
private void onBoostClick(View v){
boost.setSelected(!item.status.reblogged);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, r->{
v.startAnimation(opacityIn);
bindButton(boost, r.reblogsCount);
});
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged, null, r->boostConsumer(v, r));
}
private void boostConsumer(View v, Status r) {
v.startAnimation(opacityIn);
bindButton(boost, r.reblogsCount);
}
private boolean onBoostLongClick(View v){
v.setAlpha(1);
v.setScaleX(1);
v.setScaleY(1);
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putString("prefilledText", "\n\n" + item.status.url);
args.putInt("selectionStart", 0);
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
Context ctx = itemView.getContext();
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
AccountSession session = AccountSessionManager.getInstance().getAccount(item.accountID);
Consumer<StatusPrivacy> doReblog = (visibility) -> {
v.startAnimation(opacityOut);
session.getStatusInteractionController()
.setReblogged(item.status, !item.status.reblogged, visibility, r->boostConsumer(v, r));
dialog.dismiss();
};
View separator = menu.findViewById(R.id.separator);
TextView reblogHeader = menu.findViewById(R.id.reblog_header);
TextView undoReblog = menu.findViewById(R.id.delete_reblog);
TextView reblogAs = menu.findViewById(R.id.reblog_as);
TextView itemPublic = menu.findViewById(R.id.vis_public);
TextView itemUnlisted = menu.findViewById(R.id.vis_unlisted);
TextView itemFollowers = menu.findViewById(R.id.vis_followers);
undoReblog.setVisibility(item.status.reblogged ? View.VISIBLE : View.GONE);
separator.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
reblogHeader.setVisibility(item.status.reblogged ? View.GONE : View.VISIBLE);
reblogAs.setVisibility(AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1 ? View.VISIBLE : View.GONE);
itemPublic.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PUBLIC) ? View.GONE : View.VISIBLE);
itemUnlisted.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) ? View.GONE : View.VISIBLE);
itemFollowers.setVisibility(item.status.reblogged || item.status.visibility.isLessVisibleThan(StatusPrivacy.PRIVATE) ? View.GONE : View.VISIBLE);
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_community_24_regular);
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_checkmark_24_regular);
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
// e.g. post visibility is unlisted, but default is public
// in this case, we want to display the check mark on the most visible visibility
if (defaultVisibility != null && item.status.visibility.isLessVisibleThan(defaultVisibility)) {
for (StatusPrivacy vis : StatusPrivacy.values()) {
if (vis.equals(item.status.visibility)) {
defaultVisibility = vis;
break;
}
}
}
itemPublic.setCompoundDrawablesWithIntrinsicBounds(publicDrawable, null, StatusPrivacy.PUBLIC.equals(defaultVisibility) ? checkMark : null, null);
itemUnlisted.setCompoundDrawablesWithIntrinsicBounds(unlistedDrawable, null, StatusPrivacy.UNLISTED.equals(defaultVisibility) ? checkMark : null, null);
itemFollowers.setCompoundDrawablesWithIntrinsicBounds(followersDrawable, null, StatusPrivacy.PRIVATE.equals(defaultVisibility) ? checkMark : null, null);
undoReblog.setOnClickListener(c->doReblog.accept(null));
itemPublic.setOnClickListener(c->doReblog.accept(StatusPrivacy.PUBLIC));
itemUnlisted.setOnClickListener(c->doReblog.accept(StatusPrivacy.UNLISTED));
itemFollowers.setOnClickListener(c->doReblog.accept(StatusPrivacy.PRIVATE));
reblogAs.setOnClickListener(c->{
dialog.dismiss();
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.reblogged,
(ic, status, consumer) -> ic.setReblogged(status, true, null, consumer),
R.string.sk_reblog_as,
R.string.sk_reblogged_as,
R.string.sk_already_reblogged,
// TODO: replace once available: https://raw.githubusercontent.com/microsoft/fluentui-system-icons/main/android/library/src/main/res/drawable/ic_fluent_arrow_repeat_all_28_regular.xml
R.drawable.ic_fluent_arrow_repeat_all_24_regular
);
});
menu.findViewById(R.id.quote).setOnClickListener(c->{
dialog.dismiss();
v.startAnimation(opacityIn);
Bundle args=new Bundle();
args.putString("account", item.accountID);
StringBuilder prefilledText = new StringBuilder().append("\n\n");
String ownID = AccountSessionManager.getInstance().getAccount(item.accountID).self.id;
if (!item.status.account.id.equals(ownID)) prefilledText.append('@').append(item.status.account.acct).append(' ');
prefilledText.append(item.status.url);
args.putString("prefilledText", prefilledText.toString());
args.putInt("selectionStart", 0);
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
dialog.show();
return true;
}
private void onFavoriteClick(View v){
favorite.setSelected(!item.status.favourited);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
v.startAnimation(opacityIn);
if (item.status.favourited) {
if(GlobalUserPreferences.reduceMotion){
v.startAnimation(opacityIn);
}else{
v.startAnimation(animSet);
}
} else {
v.startAnimation(opacityIn);
}
bindButton(favorite, r.favouritesCount);
});
}
private boolean onFavoriteLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.favourited,
(ic, status, consumer) -> ic.setFavorited(status, true, consumer),
R.string.sk_favorite_as,
R.string.sk_favorited_as,
R.string.sk_already_favorited,
R.drawable.ic_fluent_star_28_regular
);
return true;
}
private void onBookmarkClick(View v){
bookmark.setSelected(item.status.bookmarked);
bookmark.setSelected(!item.status.bookmarked);
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
v.startAnimation(opacityIn);
});
}
private boolean onBookmarkLongClick(View v) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
UiUtils.pickInteractAs(v.getContext(),
item.accountID, item.status,
s -> s.bookmarked,
(ic, status, consumer) -> ic.setBookmarked(status, true, consumer),
R.string.sk_bookmark_as,
R.string.sk_bookmarked_as,
R.string.sk_already_bookmarked,
R.drawable.ic_fluent_bookmark_28_regular
);
return true;
}
private void onShareClick(View v){
v.startAnimation(opacityIn);
Intent intent=new Intent(Intent.ACTION_SEND);
@@ -208,7 +367,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private boolean onShareLongClick(View v){
UiUtils.copyText(v.getContext(), item.status.url);
UiUtils.copyText(v, item.status.url);
return true;
}
@@ -226,4 +385,4 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
return 0;
}
}
}
}

View File

@@ -9,8 +9,10 @@ import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
@@ -22,28 +24,37 @@ import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.announcements.DismissAnnouncement;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
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.ThreadFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Locale;
import java.util.function.Consumer;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
@@ -65,15 +76,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private boolean hasVisibilityToggle;
boolean needBottomPadding;
private String extraText;
private Notification notification;
private ScheduledStatus scheduledStatus;
private Announcement announcement;
private Consumer<String> consumeReadAnnouncement;
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){
public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText, Notification notification, ScheduledStatus scheduledStatus){
super(parentID, parentFragment);
user=scheduledStatus != null ? AccountSessionManager.getInstance().getAccount(accountID).self : user;
this.user=user;
this.createdAt=createdAt;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName);
this.status=status;
this.notification=notification;
this.scheduledStatus=scheduledStatus;
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
emojiHelper.setText(parsedName);
if(status!=null){
@@ -90,6 +108,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
this.extraText=extraText;
}
public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer<String> consumeReadID) {
HeaderStatusDisplayItem item = new HeaderStatusDisplayItem(a.id, instanceUser, a.startsAt, parentFragment, accountID, fakeStatus, null, null, null);
item.announcement = a;
item.consumeReadAnnouncement = consumeReadID;
return item;
}
@Override
public Type getType(){
return Type.HEADER;
@@ -109,8 +134,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView name, username, timestamp, extraText;
private final ImageView avatar, more, visibility;
private final TextView name, username, timestamp, extraText, separator;
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator;
private final PopupMenu optionsMenu;
private Relationship relationship;
private APIRequest<?> currentRelationshipRequest;
@@ -126,22 +151,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
super(activity, R.layout.display_item_header, parent);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
separator=findViewById(R.id.separator);
timestamp=findViewById(R.id.timestamp);
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
visibility=findViewById(R.id.visibility);
deleteNotification=findViewById(R.id.delete_notification);
unreadIndicator=findViewById(R.id.unread_indicator);
extraText=findViewById(R.id.extra_text);
avatar.setOnClickListener(this::onAvaClick);
avatar.setOutlineProvider(roundCornersOutline);
avatar.setClipToOutline(true);
more.setOnClickListener(this::onMoreClick);
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
deleteNotification.setOnClickListener(v->UiUtils.confirmDeleteNotification(activity, item.parentFragment.getAccountID(), item.notification, ()->{
if (item.parentFragment instanceof NotificationsListFragment fragment) {
fragment.removeNotification(item.notification);
}
}));
optionsMenu=new PopupMenu(activity, more);
optionsMenu.inflate(R.menu.post);
optionsMenu.setOnMenuItemClickListener(menuItem->{
Account account=item.user;
int id=menuItem.getItemId();
if(id==R.id.edit || id==R.id.delete_and_redraft) {
final Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
@@ -156,6 +190,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
if(TextUtils.isEmpty(item.status.content) && TextUtils.isEmpty(item.status.spoilerText)){
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else if(item.scheduledStatus!=null){
args.putString("sourceText", item.status.text);
args.putString("sourceSpoiler", item.status.spoilerText);
args.putBoolean("redraftStatus", true);
args.putParcelable("scheduledStatus", Parcels.wrap(item.scheduledStatus));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
}else{
new GetStatusSourceText(item.status.id)
.setCallback(new Callback<>(){
@@ -181,8 +221,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
.exec(item.parentFragment.getAccountID());
}
}else if(id==R.id.delete){
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}else if(id==R.id.pin || id==R.id.unpin){
if (item.scheduledStatus != null) {
UiUtils.confirmDeleteScheduledPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.scheduledStatus, ()->{});
} else {
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
}
}else if(id==R.id.pin || id==R.id.unpin) {
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
@@ -194,8 +238,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putParcelable("status", Parcels.wrap(item.status));
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
}else if(id==R.id.open_in_browser) {
UiUtils.launchWebBrowser(activity, item.status.url);
}else if(id==R.id.copy_link){
UiUtils.copyText(parent, item.status.url);
}else if(id==R.id.follow){
if(relationship==null)
return true;
@@ -209,7 +255,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
progress.dismiss();
}, rel->{
relationship=rel;
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getDisplayUsername()), Toast.LENGTH_SHORT).show();
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : R.string.unfollowed_user, account.getShortUsername()), Toast.LENGTH_SHORT).show();
});
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(activity, item.parentFragment.getAccountID(), account.getDomain(), relationship!=null && relationship.domainBlocking, ()->{});
@@ -218,17 +264,46 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
return true;
});
UiUtils.enablePopupMenuIcons(activity, optionsMenu);
}
private void populateAccountsMenu(Menu menu) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
sessions.stream().filter(s -> !s.getID().equals(item.accountID)).forEach(s -> {
String username = "@"+s.self.username+"@"+s.domain;
menu.add(username).setOnMenuItemClickListener(c->{
UiUtils.openURL(item.parentFragment.getActivity(), s.getID(), item.status.url, false);
return true;
});
});
}
@Override
public void onBind(HeaderStatusDisplayItem item){
name.setText(item.parsedName);
username.setText('@'+item.user.acct);
if(item.status==null || item.status.editedAt==null)
separator.setVisibility(View.VISIBLE);
username.setCompoundDrawablesWithIntrinsicBounds(item.user.bot ? R.drawable.ic_fluent_bot_24_filled : 0, 0, 0, 0);
if (item.scheduledStatus!=null)
if (item.scheduledStatus.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT)) {
timestamp.setText(R.string.sk_draft);
} else {
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter));
}
else if ((item.status==null || item.status.editedAt==null) && item.createdAt != null)
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
else
else if (item.status != null && item.status.editedAt != null)
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
else {
separator.setVisibility(View.GONE);
timestamp.setText("");
}
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
if(item.hasVisibilityToggle){
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
@@ -250,6 +325,42 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
currentRelationshipRequest.cancel();
}
relationship=null;
String desc;
if (item.announcement != null) {
if (unreadIndicator.getVisibility() == View.GONE) {
more.setAlpha(0f);
unreadIndicator.setAlpha(0f);
unreadIndicator.setVisibility(View.VISIBLE);
}
float alpha = item.announcement.read ? 0 : 1;
more.setImageResource(R.drawable.ic_fluent_checkmark_20_filled);
desc = item.parentFragment.getString(R.string.sk_mark_as_read);
more.animate().alpha(alpha);
unreadIndicator.animate().alpha(alpha);
more.setOnClickListener(v -> {
new DismissAnnouncement(item.announcement.id).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
item.consumeReadAnnouncement.accept(item.announcement.id);
item.announcement.read = true;
rebind();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(item.parentFragment.getActivity());
}
}).exec(item.accountID);
});
} else {
more.setImageResource(R.drawable.ic_fluent_more_vertical_20_filled);
desc = item.parentFragment.getString(R.string.more_options);
more.setOnClickListener(this::onMoreClick);
}
more.setContentDescription(desc);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) more.setTooltipText(desc);
}
@Override
@@ -270,6 +381,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void onAvaClick(View v){
if (item.announcement != null) {
UiUtils.openURL(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.user.url);
return;
}
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.user));
@@ -301,15 +416,31 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}
private void updateOptionsMenu(){
Account account=item.user;
if (item.announcement != null) return;
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
Menu menu=optionsMenu.getMenu();
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
SubMenu accountsMenu = openWithAccounts != null ? openWithAccounts.getSubMenu() : null;
if (hasMultipleAccounts && accountsMenu != null) {
openWithAccounts.setVisible(true);
accountsMenu.clear();
populateAccountsMenu(accountsMenu);
} else if (openWithAccounts != null) {
openWithAccounts.setVisible(false);
}
Account account=item.user;
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
boolean isPostScheduled=item.scheduledStatus!=null;
menu.findItem(R.id.open_with_account).setVisible(!isPostScheduled && hasMultipleAccounts);
menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
menu.findItem(R.id.delete_and_redraft).setVisible(!isPostScheduled && item.status!=null && isOwnPost);
menu.findItem(R.id.pin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && !item.status.pinned);
menu.findItem(R.id.unpin).setVisible(!isPostScheduled && item.status!=null && isOwnPost && item.status.pinned);
menu.findItem(R.id.open_in_browser).setVisible(!isPostScheduled && item.status!=null);
menu.findItem(R.id.copy_link).setVisible(!isPostScheduled && item.status!=null);
MenuItem blockDomain=menu.findItem(R.id.block_domain);
MenuItem mute=menu.findItem(R.id.mute);
MenuItem block=menu.findItem(R.id.block);
@@ -325,7 +456,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
bookmark.setVisible(false);
}
*/
if(isOwnPost){
if(isPostScheduled || isOwnPost){
mute.setVisible(false);
block.setVisible(false);
report.setVisible(false);
@@ -336,16 +467,22 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
block.setVisible(true);
report.setVisible(true);
follow.setVisible(relationship==null || relationship.following || (!relationship.blocking && !relationship.blockedBy && !relationship.domainBlocking && !relationship.muting));
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getDisplayUsername()));
if(!account.isLocal()){
blockDomain.setVisible(true);
blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
}else{
mute.setTitle(item.parentFragment.getString(relationship!=null && relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship!=null && relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), mute);
block.setTitle(item.parentFragment.getString(relationship!=null && relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
report.setTitle(item.parentFragment.getString(R.string.report_user, account.getShortUsername()));
// disabled in megalodon. domain blocks from a post clutters the context menu and looks out of place
// if(!account.isLocal()){
// blockDomain.setVisible(true);
// blockDomain.setTitle(item.parentFragment.getString(relationship!=null && relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
// }else{
blockDomain.setVisible(false);
}
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.getDisplayUsername()));
// }
boolean following = relationship!=null && relationship.following;
follow.setTitle(item.parentFragment.getString(following ? R.string.unfollow_user : R.string.follow_user, account.getShortUsername()));
follow.setIcon(following ? R.drawable.ic_fluent_person_delete_24_regular : R.drawable.ic_fluent_person_add_24_regular);
UiUtils.insetPopupMenuIcon(item.parentFragment.getContext(), follow);
}
}
}

View File

@@ -89,10 +89,13 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
button.setBackground(progressBg);
itemView.setSelected(item.isMostVoted);
icon.setSelected(item.poll.ownVotes.contains(item.poll.options.indexOf(item.option)));
icon.setVisibility(item.poll.voted && item.poll.ownVotes.isEmpty() ? View.GONE : View.VISIBLE);
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
}else{
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
icon.setSelected(itemView.isSelected());
icon.setVisibility(View.VISIBLE);
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.MastodonApp.context;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.SpannableStringBuilder;
@@ -14,6 +15,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -30,10 +32,13 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private CharSequence text;
@DrawableRes
private int icon;
private StatusPrivacy visibility;
@DrawableRes
private int iconEnd;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private View.OnClickListener handleClick;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, @Nullable View.OnClickListener handleClick){
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick){
super(parentID, parentFragment);
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
HtmlParser.parseCustomEmoji(ssb, emojis);
@@ -43,6 +48,17 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
this.handleClick=handleClick;
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
updateVisibility(visibility);
}
public void updateVisibility(StatusPrivacy visibility) {
this.visibility = visibility;
this.iconEnd = visibility != null ? switch (visibility) {
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
default -> 0;
} : 0;
}
@Override
@@ -70,10 +86,18 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
@Override
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
text.setText(item.text);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, 0, 0);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
if(item.handleClick!=null) text.setOnClickListener(item.handleClick);
text.setEnabled(!item.inset);
text.setClickable(!item.inset);
Context ctx = itemView.getContext();
int visibilityText = item.visibility != null ? switch (item.visibility) {
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
default -> 0;
} : 0;
if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")");
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
UiUtils.fixCompoundDrawableTintOnAndroid6(text);
}

View File

@@ -8,13 +8,16 @@ import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
@@ -73,26 +76,29 @@ public abstract class StatusDisplayItem{
};
}
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter){
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>();
Status statusForContent=status.getContentStatus();
Bundle args=new Bundle();
args.putString("account", accountID);
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
if(status.reblog!=null){
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, i->{
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, i->{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, null, i->{
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}));
}
HeaderStatusDisplayItem header;
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus));
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
else
@@ -170,7 +176,7 @@ public abstract class StatusDisplayItem{
}
public Holder(Context context, int layout, ViewGroup parent){
super(context, layout, parent);
super(context, layout, parent);
}
public String getItemID(){

View File

@@ -31,6 +31,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.MovieDrawable;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class TextStatusDisplayItem extends StatusDisplayItem{
@@ -155,7 +156,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
if (item.translation == null) {
translateProgress.setVisibility(View.VISIBLE);
translateButton.setClickable(false);
translateButton.setAlpha(.50f);
translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
new TranslateStatus(item.status.id).setCallback(new Callback<>() {
@Override
public void onSuccess(TranslatedStatus translatedStatus) {
@@ -163,7 +164,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
item.translated = true;
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.setAlpha(1f);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
rebind();
}
@@ -171,7 +172,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public void onError(ErrorResponse error) {
translateProgress.setVisibility(View.GONE);
translateButton.setClickable(true);
translateButton.setAlpha(1f);
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
error.showToast(itemView.getContext());
}
}).exec(item.parentFragment.getAccountID());

View File

@@ -10,6 +10,8 @@ import android.text.Layout;
import android.text.Spanned;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView;
import me.grishka.appkit.utils.V;
@@ -20,7 +22,12 @@ public class ClickableLinksDelegate {
private Path hlPath;
private LinkSpan selectedSpan;
private TextView view;
private final Runnable longClickRunnable = () -> {
if (selectedSpan != null) selectedSpan.onLongClick(view);
};
public ClickableLinksDelegate(TextView view) {
this.view=view;
hlPaint=new Paint();
@@ -30,6 +37,7 @@ public class ClickableLinksDelegate {
}
public boolean onTouch(MotionEvent event) {
long eventDuration = event.getEventTime() - event.getDownTime();
if(event.getAction()==MotionEvent.ACTION_DOWN){
int line=-1;
Rect rect=new Rect();
@@ -63,6 +71,7 @@ public class ClickableLinksDelegate {
}
hlPath=new Path();
selectedSpan=span;
view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){
@@ -90,8 +99,11 @@ public class ClickableLinksDelegate {
}
}
if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
if (eventDuration <= ViewConfiguration.getLongPressTimeout()) {
view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext());
}
view.removeCallbacks(longClickRunnable);
hlPath=null;
selectedSpan=null;
view.invalidate();
@@ -100,6 +112,7 @@ public class ClickableLinksDelegate {
if(event.getAction()==MotionEvent.ACTION_CANCEL){
hlPath=null;
selectedSpan=null;
view.removeCallbacks(longClickRunnable);
view.invalidate();
return false;
}

View File

@@ -117,8 +117,8 @@ public class HtmlParser{
case "a" -> {
String href=el.attr("href");
LinkSpan.Type linkType;
String text=el.text();
if(el.hasClass("hashtag")){
String text=el.text();
if(text.startsWith("#")){
linkType=LinkSpan.Type.HASHTAG;
href=text.substring(1);
@@ -136,7 +136,7 @@ public class HtmlParser{
}else{
linkType=LinkSpan.Type.URL;
}
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el));
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, text), ssb.length(), el));
}
case "br" -> ssb.append('\n');
case "span" -> {
@@ -260,7 +260,7 @@ public class HtmlParser{
String url=matcher.group(3);
if(TextUtils.isEmpty(matcher.group(4)))
url="http://"+url;
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0);
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, url), matcher.start(3), matcher.end(3), 0);
}while(matcher.find()); // Find more URLs
return ssb;
}

View File

@@ -1,8 +1,11 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.content.Intent;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.util.Log;
import android.view.View;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -13,12 +16,14 @@ public class LinkSpan extends CharacterStyle {
private String link;
private Type type;
private String accountID;
private String text;
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){
this.listener=listener;
this.link=link;
this.type=type;
this.accountID=accountID;
this.text=text;
}
public int getColor(){
@@ -29,7 +34,7 @@ public class LinkSpan extends CharacterStyle {
public void updateDrawState(TextPaint tp) {
tp.setColor(color=tp.linkColor);
}
public void onClick(Context context){
switch(getType()){
case URL -> UiUtils.openURL(context, accountID, link);
@@ -38,6 +43,17 @@ public class LinkSpan extends CharacterStyle {
}
}
public void onLongClick(View view) {
if (getType() == Type.URL) {
Intent shareIntent = new Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, link);
view.getContext().startActivity(Intent.createChooser(shareIntent, null));
} else {
UiUtils.copyText(view, text);
}
}
public String getLink(){
return link;
}

View File

@@ -5,6 +5,7 @@ import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
@@ -18,7 +19,6 @@ import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
@@ -27,12 +27,11 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -46,36 +45,44 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest;
import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest;
import org.joinmastodon.android.api.requests.notifications.DismissNotification;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
import org.joinmastodon.android.events.NotificationDeletedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ScheduledStatus;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.SpacerSpan;
import org.parceler.Parcels;
import java.io.File;
@@ -92,9 +99,12 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.browser.customtabs.CustomTabsIntent;
@@ -338,22 +348,29 @@ public class UiUtils{
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), onConfirmed);
showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed);
}
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, Runnable onConfirmed){
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed){
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed);
}
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, int icon, Runnable onConfirmed){
new M3AlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(confirmButton, (dlg, i)->onConfirmed.run())
.setNegativeButton(R.string.cancel, null)
.setIcon(icon)
.show();
}
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
currentlyBlocked ? R.drawable.ic_fluent_person_28_regular : R.drawable.ic_fluent_person_prohibited_28_regular,
()->{
new SetAccountBlocked(account.id, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
@@ -377,7 +394,9 @@ public class UiUtils{
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), ()->{
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
R.drawable.ic_fluent_shield_28_regular,
()->{
new SetDomainBlocked(domain, !currentlyBlocked)
.setCallback(new Callback<>(){
@Override
@@ -398,7 +417,9 @@ public class UiUtils{
public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback){
showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title),
activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName),
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), ()->{
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute),
currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular,
()->{
new SetAccountMuted(account.id, !currentlyMuted)
.setCallback(new Callback<>(){
@Override
@@ -423,24 +444,53 @@ public class UiUtils{
}
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){
showConfirmationAlert(activity, forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title, forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete, forRedraft ? R.string.sk_delete_and_redraft : R.string.delete, ()->{
new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
showConfirmationAlert(activity,
forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title,
forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete,
forRedraft ? R.string.sk_delete_and_redraft : R.string.delete,
forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID);
});
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID)
);
}
public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback){
boolean isDraft = status.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT);
showConfirmationAlert(activity,
isDraft ? R.string.sk_confirm_delete_draft_title : R.string.sk_confirm_delete_scheduled_post_title,
isDraft ? R.string.sk_confirm_delete_draft : R.string.sk_confirm_delete_scheduled_post,
R.string.delete,
R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus.Scheduled(status.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object nothing){
resultCallback.run();
E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error){
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.deleting, false)
.exec(accountID)
);
}
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
@@ -448,6 +498,7 @@ public class UiUtils{
pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title,
pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post,
pinned ? R.string.sk_pin_post : R.string.sk_unpin_post,
pinned ? R.drawable.ic_fluent_pin_28_regular : R.drawable.ic_fluent_pin_off_28_regular,
()->{
new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() {
@@ -470,6 +521,26 @@ public class UiUtils{
);
}
public static void confirmDeleteNotification(Activity activity, String accountID, Notification notification, Runnable callback) {
showConfirmationAlert(activity,
notification == null ? R.string.sk_clear_all_notifications : R.string.sk_delete_notification,
notification == null ? R.string.sk_clear_all_notifications_confirm : R.string.sk_delete_notification_confirm,
notification == null ? R.string.sk_clear_all_notifications_confirm_action : R.string.sk_delete_notification_confirm_action,
notification == null ? R.drawable.ic_fluent_mail_inbox_dismiss_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DismissNotification(notification != null ? notification.id : null).setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {
callback.run();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
}).exec(accountID)
);
}
public static void setRelationshipToActionButton(Relationship relationship, Button button){
setRelationshipToActionButton(relationship, button, false);
}
@@ -637,6 +708,42 @@ public class UiUtils{
return bitmap;
}
public static void insetPopupMenuIcon(Context context, MenuItem item) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
insetPopupMenuIcon(item, iconTint);
}
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) {
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint);
else icon.setTintList(iconTint);
icon=new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
item.setTitle(ssb);
}
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
if(menu.getClass().getSimpleName().equals("MenuBuilder")){
try {
Method m = menu.getClass().getDeclaredMethod(
"setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
m.invoke(menu, true);
enableMenuIcons(context, menu, asAction);
}
catch(Exception ignored){}
}
}
public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclude) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue;
insetPopupMenuIcon(item, iconTint);
}
}
public static void enablePopupMenuIcons(Context context, PopupMenu menu){
Menu m=menu.getMenu();
if(Build.VERSION.SDK_INT>=29){
@@ -648,23 +755,7 @@ public class UiUtils{
setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){}
}
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26){
item.setIconTintList(iconTint);
}else{
icon.setTintList(iconTint);
}
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
ssb.insert(0, " ");
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
item.setTitle(ssb);
}
enableMenuIcons(context, m);
}
public static void setUserPreferredTheme(Context context){
@@ -711,29 +802,107 @@ public class UiUtils{
String it = uri.getPath();
return it.matches("^/@[^/]+$") ||
it.matches("^/@[^/]+/\\d+$") ||
it.matches("^/users/\\w+$") ||
it.matches("^/notice/[a-zA-Z0-9]+$") ||
it.matches("^/objects/[-a-f0-9]+$") ||
it.matches("^/notes/[a-z0-9]+$") ||
it.matches("^/display/[-a-f0-9]+$") ||
it.matches("^/profile/\\w+$") ||
it.matches("^/p/\\w+/\\d+$") ||
it.matches("^/\\w+$") ||
it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$") ||
it.matches("^/o/[a-f0-9]+$");
it.matches("^/@[^/]+/\\d+$") ||
it.matches("^/users/\\w+$") ||
it.matches("^/notice/[a-zA-Z0-9]+$") ||
it.matches("^/objects/[-a-f0-9]+$") ||
it.matches("^/notes/[a-z0-9]+$") ||
it.matches("^/display/[-a-f0-9]+$") ||
it.matches("^/profile/\\w+$") ||
it.matches("^/p/\\w+/\\d+$") ||
it.matches("^/\\w+$") ||
it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$") ||
it.matches("^/users/[^/]+/statuses/[a-zA-Z0-9]+$") ||
it.matches("^/o/[a-f0-9]+$");
}
public static void openURL(Context context, String accountID, String url){
Consumer<ProgressDialog> transformDialogForLookup = dialog -> {
dialog.setTitle(R.string.loading_fediverse_resource_title);
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), (d, which) -> d.cancel());
public static String getInstanceName(String accountID) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
}
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s->!s.getID().equals(exceptFor)).collect(Collectors.toList());
AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
.setItems(
sessions.stream().map(AccountSession::getFullUsername).toArray(String[]::new),
(dialog, which) -> sessionConsumer.accept(sessions.get(which))
)
.setTitle(titleRes == 0 ? R.string.choose_account : titleRes)
.setIcon(iconRes);
if (transformDialog != null) transformDialog.accept(builder);
builder.show();
}
@FunctionalInterface
public interface InteractionPerformer {
void interact(StatusInteractionController ic, Status status, Consumer<Status> resultConsumer);
}
public static void pickInteractAs(Context context, String accountID, Status sourceStatus, Predicate<Status> checkInteracted, InteractionPerformer interactionPerformer, @StringRes int interactAsRes, @StringRes int interactedAsAccountRes, @StringRes int alreadyInteractedRes, @DrawableRes int iconRes) {
pickAccount(context, accountID, interactAsRes, iconRes, session -> {
lookupStatus(context, sourceStatus, session.getID(), accountID, status -> {
if (checkInteracted.test(status)) {
Toast.makeText(context, alreadyInteractedRes, Toast.LENGTH_SHORT).show();
return;
}
StatusInteractionController ic = AccountSessionManager.getInstance().getAccount(session.getID()).getRemoteStatusInteractionController();
interactionPerformer.interact(ic, status, s -> {
if (checkInteracted.test(s)) {
Toast.makeText(context, context.getString(interactedAsAccountRes, session.getFullUsername()), Toast.LENGTH_SHORT).show();
}
});
});
}, null);
}
public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer<Status> statusConsumer) {
if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) {
statusConsumer.accept(queryStatus);
return;
}
new GetSearchResults(queryStatus.url, GetSearchResults.Type.STATUSES, true).setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0));
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
}
public static void openURL(Context context, String accountID, String url) {
openURL(context, accountID, url, true);
}
private static void transformDialogForLookup(Context context, String accountID, @Nullable String url, ProgressDialog dialog) {
if (accountID != null) {
dialog.setTitle(context.getString(R.string.sk_loading_resource_on_instance_title, getInstanceName(accountID)));
} else {
dialog.setTitle(R.string.sk_loading_fediverse_resource_title);
}
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.cancel), (d, which) -> d.cancel());
if (url != null) {
dialog.setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.open_in_browser), (d, which) -> {
d.cancel();
launchWebBrowser(context, url);
});
};
}
}
public static void openURL(Context context, String accountID, String url, boolean launchBrowser){
Uri uri=Uri.parse(url);
List<String> path=uri.getPathSegments();
if(accountID!=null && "https".equals(uri.getScheme())){
@@ -751,10 +920,11 @@ public class UiUtils{
@Override
public void onError(ErrorResponse error){
error.showToast(context);
launchWebBrowser(context, url);
if (launchBrowser) launchWebBrowser(context, url);
}
})
.wrapProgress((Activity)context, R.string.loading, true, transformDialogForLookup)
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
} else if (looksLikeMastodonUrl(url)) {
@@ -771,17 +941,19 @@ public class UiUtils{
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
Nav.go((Activity) context, ProfileFragment.class, args);
} else {
launchWebBrowser(context, url);
if (launchBrowser) launchWebBrowser(context, url);
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
launchWebBrowser(context, url);
if (launchBrowser) launchWebBrowser(context, url);
}
})
.wrapProgress((Activity)context, R.string.loading, true, transformDialogForLookup)
.wrapProgress((Activity)context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
}
@@ -789,14 +961,13 @@ public class UiUtils{
launchWebBrowser(context, url);
}
public static void copyText(Context context, String text) {
public static void copyText(View v, String text) {
Context context = v.getContext();
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
}
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE));
else vibrator.vibrate(50);
v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
private static String getSystemProperty(String key){
@@ -811,4 +982,35 @@ public class UiUtils{
public static boolean isMIUI(){
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
}
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText){
Bundle args = new Bundle();
if (prefilledText != null) args.putString("prefilledText", prefilledText);
return pickAccountForCompose(activity, accountID, args);
}
public static boolean pickAccountForCompose(Activity activity, String accountID){
return pickAccountForCompose(activity, accountID, (String) null);
}
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args){
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) {
UiUtils.pickAccount(activity, accountID, 0, 0, session -> {
args.putString("account", session.getID());
Nav.go(activity, ComposeFragment.class, args);
}, null);
return true;
} else {
return false;
}
}
public static String getVisibilityText(Status status) {
return MastodonApp.context.getString(switch (status.visibility) {
case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only;
case DIRECT -> R.string.visibility_private;
});
}
}

View File

@@ -4,6 +4,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import java.time.Instant;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -21,6 +22,16 @@ public class StatusFilterPredicate implements Predicate<Status>{
@Override
public boolean test(Status status){
if(status.filtered!=null){
if (status.filtered.isEmpty()){
return true;
}
boolean matches=status.filtered.stream()
.map(filterResult->filterResult.filter)
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
.anyMatch(filter->filter.filterAction==Filter.FilterAction.HIDE);
return !matches;
}
for(Filter filter:filters){
if(filter.matches(status))
return false;