Merge remote-tracking branch 'megalodon_main/main'

# Conflicts:
#	mastodon/build.gradle
#	mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java
#	mastodon/src/main/java/org/joinmastodon/android/MainActivity.java
#	mastodon/src/main/java/org/joinmastodon/android/PushNotificationReceiver.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowerListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/FollowingListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusFavoritesListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/StatusReblogsListFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverAccountsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverHashtagsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverNewsFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java
#	mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HashtagStatusDisplayItem.java
#	mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java
#	mastodon/src/main/res/layout/item_account_switcher.xml
#	mastodon/src/main/res/values-ar-rDZ/strings_sk.xml
#	mastodon/src/main/res/values-es-rES/strings_sk.xml
#	mastodon/src/main/res/values-nl-rNL/strings_sk.xml
#	mastodon/src/main/res/values-pt-rPT/strings_sk.xml
#	mastodon/src/main/res/values-v31/colors.xml
#	mastodon/src/main/res/values/colors.xml
#	mastodon/src/main/res/values/styles.xml
This commit is contained in:
LucasGGamerM
2023-06-03 19:56:16 -03:00
113 changed files with 4439 additions and 1513 deletions

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
@@ -19,6 +18,7 @@ import org.jsoup.internal.StringUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
@@ -30,8 +30,8 @@ public class ExternalShareActivity extends FragmentStackActivity{
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
boolean isMastodonURL = UiUtils.looksLikeMastodonUrl(text);
Optional<String> text = Optional.ofNullable(getIntent().getStringExtra(Intent.EXTRA_TEXT));
boolean isMastodonURL = text.map(UiUtils::looksLikeMastodonUrl).orElse(false);
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();
if(sessions.isEmpty()){
@@ -40,11 +40,22 @@ public class ExternalShareActivity extends FragmentStackActivity{
}else if(sessions.size()==1 && !isMastodonURL){
openComposeFragment(sessions.get(0).getID());
}else{
new AccountSwitcherSheet(this, false, false, isMastodonURL, accountSession -> {
if(accountSession!=null)
openComposeFragment(accountSession.getID());
else
UiUtils.openURL(this, AccountSessionManager.getInstance().getLastActiveAccountID(), text);
new AccountSwitcherSheet(this, null, true, isMastodonURL, (accountId, open) -> {
if (open && text.isPresent()) {
UiUtils.lookupURL(this, accountId, text.get(), false, (clazz, args) -> {
if (clazz == null) {
finish();
return;
}
args.putString("fromExternalShare", clazz.getSimpleName());
Intent intent = new Intent(this, MainActivity.class);
intent.putExtras(args);
finish();
startActivity(intent);
});
} else {
openComposeFragment(accountId);
}
}).show();
}
}
@@ -108,11 +119,4 @@ public class ExternalShareActivity extends FragmentStackActivity{
return null;
return new ArrayList<>(l);
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
}
}

View File

@@ -88,6 +88,16 @@ public class GlobalUserPreferences{
catch (JsonSyntaxException ignored) { return orElse; }
}
public static void removeAccount(String accountId) {
recentLanguages.remove(accountId);
pinnedTimelines.remove(accountId);
accountsInGlitchMode.remove(accountId);
accountsWithLocalOnlySupport.remove(accountId);
accountsWithContentTypesEnabled.remove(accountId);
accountsDefaultContentTypes.remove(accountId);
save();
}
public static void load(){
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
@@ -218,4 +228,3 @@ public class GlobalUserPreferences{
DARK
}
}

View File

@@ -9,6 +9,8 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.session.AccountSession;
@@ -22,13 +24,13 @@ 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;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
public class MainActivity extends FragmentStackActivity{
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
@@ -38,10 +40,18 @@ public class MainActivity extends FragmentStackActivity{
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
showFragmentClearingBackStack(new CustomWelcomeFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session;
Bundle args=new Bundle();
Intent intent=getIntent();
if(intent.hasExtra("fromExternalShare")) {
AccountSessionManager.getInstance()
.setLastActiveAccountID(intent.getStringExtra("account"));
AccountSessionManager.getInstance().maybeUpdateLocalInfo(
AccountSessionManager.getInstance().getLastActiveAccount());
showFragmentForExternalShare(intent.getExtras());
return;
}
boolean fromNotification = intent.getBooleanExtra("fromNotification", false);
boolean hasNotification = intent.hasExtra("notification");
if(fromNotification){
@@ -55,6 +65,7 @@ public class MainActivity extends FragmentStackActivity{
}else{
session=AccountSessionManager.getInstance().getLastActiveAccount();
}
AccountSessionManager.getInstance().maybeUpdateLocalInfo(session);
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
@@ -78,12 +89,12 @@ public class MainActivity extends FragmentStackActivity{
@Override
protected void onNewIntent(Intent intent){
super.onNewIntent(intent);
if(intent.getBooleanExtra("fromNotification", false)){
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
if (intent.hasExtra("fromExternalShare")) showFragmentForExternalShare(intent.getExtras());
else if (intent.getBooleanExtra("fromNotification", false)) {
String accountID=intent.getStringExtra("accountID");
AccountSession accountSession;
try{
accountSession=AccountSessionManager.getInstance().getAccount(accountID);
DomainManager.getInstance().setCurrentDomain(accountSession.domain);
AccountSessionManager.getInstance().getAccount(accountID);
}catch(IllegalStateException x){
return;
}
@@ -128,6 +139,19 @@ public class MainActivity extends FragmentStackActivity{
showFragment(fragment);
}
private void showFragmentForExternalShare(Bundle args) {
String clazz = args.getString("fromExternalShare");
Fragment fragment = switch (clazz) {
case "ThreadFragment" -> new ThreadFragment();
case "ProfileFragment" -> new ProfileFragment();
default -> null;
};
if (fragment == null) return;
args.putBoolean("_can_go_back", true);
fragment.setArguments(args);
showFragment(fragment);
}
private void showCompose(){
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
if(session==null || !session.activated)
@@ -157,25 +181,40 @@ public class MainActivity extends FragmentStackActivity{
(fragmentContainers.get(fragmentContainers.size() - 1)).getId()
);
Bundle currentArgs = currentFragment.getArguments();
if (this.fragmentContainers.size() == 1
&& currentArgs != null
&& currentArgs.getBoolean("_can_go_back", false)
&& currentArgs.containsKey("account")) {
if (fragmentContainers.size() != 1
|| currentArgs == null
|| !currentArgs.getBoolean("_can_go_back", false)) {
super.onBackPressed();
return;
}
if (currentArgs.getBoolean("_finish_on_back", false)) {
finish();
} else if (currentArgs.containsKey("account")) {
Bundle args = new Bundle();
args.putString("account", currentArgs.getString("account"));
args.putString("tab", "notifications");
if (getIntent().getBooleanExtra("fromNotification", false)) {
args.putString("tab", "notifications");
}
Fragment fragment=new HomeFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
} else {
super.onBackPressed();
}
}
@Override
public void onProvideAssistContent(AssistContent outContent) {
super.onProvideAssistContent(outContent);
outContent.setWebUri(Uri.parse(DomainManager.getInstance().getCurrentDomain()));
public Fragment getCurrentFragment() {
for (int i = fragmentContainers.size() - 1; i >= 0; i--) {
FrameLayout fl = fragmentContainers.get(i);
if (fl.getVisibility() == View.VISIBLE) {
return getFragmentManager().findFragmentById(fl.getId());
}
}
return null;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
super.onProvideAssistContent(assistContent);
Fragment fragment = getCurrentFragment();
if (fragment != null) callFragmentToProvideAssistContent(fragment, assistContent);
}
}

View File

@@ -29,6 +29,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Mention;
import org.joinmastodon.android.model.NotificationAction;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushNotification;
@@ -38,6 +39,7 @@ import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@@ -57,7 +59,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
private static final String ACTION_KEY_TEXT_REPLY = "ACTION_KEY_TEXT_REPLY";
private static final int SUMMARY_ID = 791;
private static int notificationId;
private static int notificationId = 0;
@Override
public void onReceive(Context context, Intent intent){
@@ -298,27 +300,60 @@ public class PushNotificationReceiver extends BroadcastReceiver{
}
CharSequence input = remoteInput.getCharSequence(ACTION_KEY_TEXT_REPLY);
// copied from ComposeFragment - TODO: generalize?
ArrayList<String> mentions=new ArrayList<>();
Status status = notification.status;
String ownID=AccountSessionManager.getInstance().getAccount(accountID).self.id;
if(!status.account.id.equals(ownID))
mentions.add('@'+status.account.acct);
for(Mention mention:status.mentions){
if(mention.id.equals(ownID))
continue;
String m='@'+mention.acct;
if(!mentions.contains(m))
mentions.add(m);
}
String initialText=mentions.isEmpty() ? "" : TextUtils.join(" ", mentions)+" ";
CreateStatus.Request req=new CreateStatus.Request();
req.status = input.toString() + "\n\n" + "@" + notification.status.account.acct;
req.language = notification.status.language;
req.visibility = (notification.status.visibility == StatusPrivacy.PUBLIC && GlobalUserPreferences.defaultToUnlistedReplies ? StatusPrivacy.UNLISTED : notification.status.visibility);
req.status = initialText + input.toString();
req.language = preferences.postingDefaultLanguage;
req.visibility = preferences.postingDefaultVisibility;
req.inReplyToId = notification.status.id;
if(!notification.status.spoilerText.isEmpty() && GlobalUserPreferences.prefixRepliesWithRe && !notification.status.spoilerText.startsWith("re: ")){
req.spoilerText = "re: " + notification.status.spoilerText;
}
new CreateStatus(req, UUID.randomUUID().toString()).exec(accountID);
new CreateStatus(req, UUID.randomUUID().toString()).setCallback(new Callback<>() {
@Override
public void onSuccess(Status status) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
new Notification.Builder(context, accountID+"_"+notification.type) :
new Notification.Builder(context)
.setPriority(Notification.PRIORITY_DEFAULT)
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder builder = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ?
new Notification.Builder(context, accountID+"_"+notification.type) :
new Notification.Builder(context)
.setPriority(Notification.PRIORITY_DEFAULT)
.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
notification.status = status;
Intent contentIntent=new Intent(context, MainActivity.class);
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
contentIntent.putExtra("fromNotification", true);
contentIntent.putExtra("accountID", accountID);
contentIntent.putExtra("notification", Parcels.wrap(notification));
Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo)
.setContentText(context.getString(R.string.mo_notification_action_replied, notification.status.account.getDisplayUsername()))
.build();
notificationManager.notify(accountID, notificationId, repliedNotification);
Notification repliedNotification = builder.setSmallIcon(R.drawable.ic_ntf_logo)
.setContentTitle(context.getString(R.string.sk_notification_action_replied, notification.status.account.displayName))
.setContentText(status.getStrippedText())
.setCategory(Notification.CATEGORY_SOCIAL)
.setContentIntent(PendingIntent.getActivity(context, notificationId, contentIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT))
.build();
notificationManager.notify(accountID, notificationId, repliedNotification);
}
@Override
public void onError(ErrorResponse errorResponse) {
}
}).exec(accountID);
}
}
}

View File

@@ -19,7 +19,6 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -160,7 +159,7 @@ public class CacheController{
}
}
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma())
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){

View File

@@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.notifications;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
private String maxID;
public PleromaMarkNotificationsRead(String maxID) {
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
this.maxID = maxID;
}
@Override
public RequestBody getRequestBody() {
MultipartBody.Builder builder=new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if(!TextUtils.isEmpty(maxID))
builder.addFormDataPart("max_id", maxID);
return builder.build();
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.timelines;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
public GetBubbleTimeline(String maxID, int limit) {
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.api.session;
import android.net.Uri;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
@@ -7,6 +9,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
@@ -14,6 +17,7 @@ import org.joinmastodon.android.model.Token;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class AccountSession{
public Token token;
@@ -87,4 +91,15 @@ public class AccountSession{
pushSubscriptionManager=new PushSubscriptionManager(getID());
return pushSubscriptionManager;
}
public Optional<Instance> getInstance() {
return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
}
public Uri getInstanceUri() {
return new Uri.Builder()
.scheme("https")
.authority(getInstance().map(i -> i.normalizedUri).orElse(domain))
.build();
}
}

View File

@@ -15,6 +15,7 @@ import android.util.Log;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
@@ -121,6 +122,12 @@ public class AccountSessionManager{
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
// write initial instance info to file immediately to avoid sessions without instance info
InstanceInfoStorageWrapper wrapper = new InstanceInfoStorageWrapper();
wrapper.instance = instance;
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
updateMoreInstanceInfo(instance, instance.uri);
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
@@ -129,14 +136,16 @@ public class AccountSessionManager{
}
public synchronized void writeAccountsFile(){
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
File tmpFile = new File(MastodonApp.context.getFilesDir(), "accounts.json~");
File file = new File(MastodonApp.context.getFilesDir(), "accounts.json");
try{
try(FileOutputStream out=new FileOutputStream(file)){
try(FileOutputStream out=new FileOutputStream(tmpFile)){
SessionsStorageWrapper w=new SessionsStorageWrapper();
w.accounts=new ArrayList<>(sessions.values());
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(w, writer);
writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
}
}catch(IOException x){
Log.e(TAG, "Error writing accounts file", x);
@@ -189,6 +198,7 @@ public class AccountSessionManager{
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
MastodonApp.context.deleteDatabase(id+".db");
GlobalUserPreferences.removeAccount(id);
sessions.remove(id);
if(lastActiveAccountID.equals(id)){
if(sessions.isEmpty())
@@ -259,31 +269,35 @@ public class AccountSessionManager{
}
public void maybeUpdateLocalInfo(){
maybeUpdateLocalInfo(null);
}
public void maybeUpdateLocalInfo(AccountSession activeSession){
long now=System.currentTimeMillis();
HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase());
// if(now-session.infoLastUpdated>24L*3600_000L){
updateSessionPreferences(session);
updateSessionLocalInfo(session);
// }
// if(now-session.filtersLastUpdated>3600_000L){
updateSessionWordFilters(session);
// }
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
updateSessionPreferences(session);
updateSessionLocalInfo(session);
}
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
updateSessionWordFilters(session);
}
updateSessionMarkers(session);
}
if(loadedInstances){
maybeUpdateCustomEmojis(domains);
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
}
}
private void maybeUpdateCustomEmojis(Set<String> domains){
private void maybeUpdateCustomEmojis(Set<String> domains, String activeDomain){
long now=System.currentTimeMillis();
for(String domain:domains){
// Long lastUpdated=instancesLastUpdated.get(domain);
// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateInstanceInfo(domain);
// }
Long lastUpdated=instancesLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
updateInstanceInfo(domain);
}
}
}
@@ -411,7 +425,9 @@ public class AccountSessionManager{
@Override
public void onError(ErrorResponse error){
InstanceInfoStorageWrapper wrapper=new InstanceInfoStorageWrapper();
wrapper.instance = instance;
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, domain));
}
})
.execNoAuth(domain);
@@ -422,10 +438,13 @@ public class AccountSessionManager{
}
private void writeInstanceInfoFile(InstanceInfoStorageWrapper emojis, String domain){
try(FileOutputStream out=new FileOutputStream(getInstanceInfoFile(domain))){
File file = getInstanceInfoFile(domain);
File tmpFile = new File(file.getPath() + "~");
try(FileOutputStream out=new FileOutputStream(tmpFile)){
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
MastodonAPIController.gson.toJson(emojis, writer);
writer.flush();
if (!tmpFile.renameTo(file)) Log.e(TAG, "Error renaming " + tmpFile.getPath() + " to " + file.getPath());
}catch(IOException x){
Log.w(TAG, "Error writing instance info file for "+domain, x);
}
@@ -445,7 +464,7 @@ public class AccountSessionManager{
}
if(!loadedInstances){
loadedInstances=true;
maybeUpdateCustomEmojis(domains);
maybeUpdateCustomEmojis(domains, null);
}
}
@@ -469,10 +488,6 @@ public class AccountSessionManager{
return instances.get(domain);
}
public Instance getInstanceInfoForAccount(String account) {
return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain);
}
public void updateAccountInfo(String id, Account account){
AccountSession session=getAccount(id);
session.self=account;

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -131,4 +132,13 @@ public class AccountTimelineFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
@Override
public Uri getWebUri(Uri.Builder base) {
// could return different uris based on filter (e.g. media -> "/media"), but i want to
// return the remote url to the user, and i don't know whether i'd need to append
// '#media' (akkoma/pleroma) or '/media' (glitch/mastodon) since i don't know anything
// about the remote instance. so, just returning the base url to the user instead
return Uri.parse(user.url);
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import static java.util.stream.Collectors.toList;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -103,4 +104,9 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
})
.exec(accountID);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? base.path("/announcements").build() : null;
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -15,6 +16,7 @@ import android.text.TextPaint;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
@@ -49,6 +51,7 @@ import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.util.ArrayList;
@@ -69,7 +72,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, DomainDisplay{
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri, DomainDisplay {
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter;
protected String accountID;
@@ -132,7 +135,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
displayItems.clear();
}
protected void prependItems(List<T> items, boolean notify){
protected int prependItems(List<T> items, boolean notify){
data.addAll(0, items);
int offset=0;
for(T s:items){
@@ -145,6 +148,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
if(notify)
adapter.notifyItemRangeInserted(0, offset);
return offset;
}
protected String getMaxID(){
@@ -205,7 +209,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){
if(holder!=null && list!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
int[] pos={0, 0};
@@ -337,6 +341,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
private Rect tmpRect=new Rect();
@Override
public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
list.getDecoratedBoundsWithMargins(view, outRect);
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
@@ -348,18 +354,40 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
if(holder instanceof StatusDisplayItem.Holder<?> h){
String otherID=((StatusDisplayItem.Holder<?>) holder).getItemID();
if(otherID.equals(id)){
if (firstIndex < 0) firstIndex = i;
lastIndex = i;
StatusDisplayItem item = h.getItem();
hasDescendant = item.hasDescendantNeighbor;
// no for direct descendants because main status (right above) is
// being displayed with an extended footer - no connected layout
hasAncestor = item.hasAncestoringNeighbor && !item.isDirectDescendant;
list.getDecoratedBoundsWithMargins(child, tmpRect);
outRect.left=Math.min(outRect.left, tmpRect.left);
outRect.top=Math.min(outRect.top, tmpRect.top);
outRect.right=Math.max(outRect.right, tmpRect.right);
outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom);
if (holder instanceof WarningFilteredStatusDisplayItem.Holder) {
isWarning = true;
}
}
}
}
}
// shifting the selection box down
// see also: FooterStatusDisplayItem#onBind (setMargins)
if (isWarning || firstIndex < 0 || lastIndex < 0) return;
int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1;
boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(prevIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() &&
list.getChildViewHolder(list.getChildAt(nextIndex))
instanceof WarningFilteredStatusDisplayItem.Holder;
if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4);
if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4);
}
});
list.setItemAnimator(new BetterItemAnimator());
@@ -568,6 +596,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
holder.rebind();
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
}
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
@@ -579,6 +615,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
warning.getItem().status.filterRevealed = true;
}
@Override
public String getAccountID(){
return accountID;
}
@@ -717,6 +754,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return attachmentViewsPool;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
@@ -778,6 +819,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
@@ -41,4 +42,9 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/bookmarks").build();
}
}

View File

@@ -252,10 +252,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
accountID=getArguments().getString("account");
contentType = GlobalUserPreferences.accountsDefaultContentTypes.get(accountID);
if (contentType == null && GlobalUserPreferences.accountsWithContentTypesEnabled.contains(accountID)) {
// if formatting is enabled, use plain to avoid confusing unspecified default setting
contentType = ContentType.PLAIN;
}
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self;
@@ -274,9 +270,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Nav.finish(this);
return;
}
if(customEmojis.isEmpty()){
AccountSessionManager.getInstance().updateInstanceInfo(instanceDomain);
}
Bundle bundle = savedInstanceState != null ? savedInstanceState : getArguments();
if (bundle.containsKey("scheduledStatus")) scheduledStatus=Parcels.unwrap(bundle.getParcelable("scheduledStatus"));
@@ -1146,7 +1139,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
req.status=text;
req.localOnly=localOnly;
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
req.visibility=localOnly && instance.isAkkoma() ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.contentType=contentType;
@@ -1800,11 +1793,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollChanged=true;
updatePublishButtonState();
}));
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
int maxCharactersPerOption = 50;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
maxCharactersPerOption = instance.configuration.polls.maxCharactersPerOption;
else if(instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0)
maxCharactersPerOption = instance.pollLimits.maxOptionChars;
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCharactersPerOption)});
pollOptionsView.addView(option.view);
pollOptions.add(option);
if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4))
int maxPollOptions = 4;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
maxPollOptions = instance.configuration.polls.maxOptions;
else if (instance.pollLimits!=null && instance.pollLimits.maxOptions>0)
maxPollOptions = instance.pollLimits.maxOptions;
if(pollOptions.size()==maxPollOptions)
addPollOptionBtn.setVisibility(View.GONE);
return option;
}
@@ -1961,7 +1967,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Menu m=visibilityPopup.getMenu();
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.pleroma != null) {
if (instance.isAkkoma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);

View File

@@ -32,9 +32,12 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CustomLocalTimeline;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.DividerItemDecoration;
@@ -196,7 +199,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
@@ -222,7 +225,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
updateOptionsMenu();
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
@@ -41,4 +42,11 @@ public class FavoritedStatusListFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.encodedPath(isInstanceAkkoma()
? '/' + getSession().self.username + "#favorites"
: "/favourites").build();
}
}

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@@ -24,6 +25,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.Collections;
@@ -46,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop{
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@@ -149,8 +151,13 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/friend-requests" : "/follow_requests").build();
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@@ -14,14 +15,15 @@ import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop {
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String nextMaxID;
private String accountId;
private String accountID;
public FollowedHashtagsFragment() {
super(20);
@@ -31,7 +33,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
accountID=args.getString("account");
setTitle(R.string.sk_hashtags_you_follow);
}
@@ -62,7 +64,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
onDataLoaded(result, nextMaxID!=null);
}
})
.exec(accountId);
.exec(accountID);
}
@Override
@@ -76,8 +78,13 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/followed_tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@@ -114,7 +121,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
@Override
public void onClick() {
UiUtils.openHashtagTimeline(getActivity(), accountId, item.name, item.following);
UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following);
}
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.fragments;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import java.util.Optional;
public interface HasAccountID {
String getAccountID();
default AccountSession getSession() {
return AccountSessionManager.getInstance().getAccount(getAccountID());
}
default boolean isInstanceAkkoma() {
return getInstance().map(Instance::isAkkoma).orElse(false);
}
default Optional<Instance> getInstance() {
return getSession().getInstance();
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.Menu;
@@ -8,7 +9,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import org.joinmastodon.android.DomainManager;
@@ -167,4 +167,9 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build();
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Fragment;
import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.graphics.Outline;
import android.os.Build;
@@ -40,16 +41,13 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import java.util.Optional;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
@@ -63,11 +61,9 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
public class HomeFragment extends AppKitFragment implements OnBackPressedListener, ProvidesAssistContent, HasAccountID {
private FragmentRootLinearLayout content;
private HomeTabFragment homeTabFragment;
private NotificationsFragment notificationsFragment;
private DiscoverFragment searchFragment;
private ProfileFragment profileFragment;
@@ -79,6 +75,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private int currentTab=R.id.tab_home;
private String accountID;
private boolean isPleroma;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -86,18 +83,21 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
E.register(this);
accountID=getArguments().getString("account");
setTitle(R.string.mo_app_name);
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
.map(Instance::isAkkoma)
.orElse(false);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
// TODO: clean up
if(savedInstanceState==null){
Bundle args=new Bundle();
args.putString("account", accountID);
homeTabFragment=new HomeTabFragment();
homeTabFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("disableDiscover", isPleroma);
args.putBoolean("noAutoLoad", true);
searchFragment=new DiscoverFragment();
searchFragment.setArguments(args);
@@ -149,7 +149,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
.commit();
String defaultTab=getArguments().getString("tab");
if("notifications".equals(defaultTab)){
tabBar.selectTab(R.id.tab_notifications);
@@ -170,19 +169,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public void onViewStateRestored(Bundle savedInstanceState){
super.onViewStateRestored(savedInstanceState);
if(savedInstanceState==null) return;
homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment");
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab");
tabBar.selectTab(currentTab);
Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction()
.hide(homeTabFragment)
.hide(searchFragment)
@@ -190,15 +184,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
.hide(profileFragment)
.show(current)
.commit();
maybeTriggerLoading(current);
}
@Override
public void onHiddenChanged(boolean hidden){
super.onHiddenChanged(hidden);
if (!hidden && fragmentForTab(currentTab) instanceof DomainDisplay display)
DomainManager.getInstance().setCurrentDomain(display.getDomain());
fragmentForTab(currentTab).onHiddenChanged(hidden);
}
@@ -222,9 +213,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
homeTabFragment.onApplyWindowInsets(topOnlyInsets);
searchFragment.onApplyWindowInsets(topOnlyInsets);
notificationsFragment.onApplyWindowInsets(topOnlyInsets);
profileFragment.onApplyWindowInsets(topOnlyInsets);
@@ -243,34 +232,28 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
throw new IllegalArgumentException();
}
public void setCurrentTab(@IdRes int tab){
if(tab==currentTab)
return;
tabBar.selectTab(tab);
onTabSelected(tab);
}
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)
if (tab == R.id.tab_search)
searchFragment.onSelect();
else if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
return;
}
if(tab==currentTab && tab == R.id.tab_search){
if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
return;
}
if (newFragment instanceof DomainDisplay display) {
DomainManager.getInstance().setCurrentDomain(display.getDomain());
}
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
maybeTriggerLoading(newFragment);
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
}
private void maybeTriggerLoading(Fragment newFragment){
@@ -297,10 +280,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new AccountSwitcherSheet(getActivity(), true, true, false, accountSession -> {
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}).show();
new AccountSwitcherSheet(getActivity(), this).show();
return true;
}
if(tab==R.id.tab_search){
@@ -336,7 +316,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putInt("selectedTab", currentTab);
if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment);
if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
@@ -345,10 +324,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
if (instance == null) return;
Optional<Instance> instance = session.getInstance();
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.pleroma != null)
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {
@@ -356,9 +335,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
try {
long newestId = Long.parseLong(notifications.get(0).id);
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
System.out.println("NEWEST: " + newestId);
System.out.println("LAST SEEN: " + lastSeenId);
setNotificationBadge(newestId > lastSeenId);
} catch (Exception ignored) {
setNotificationBadge(false);
@@ -372,6 +348,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
}).exec(accountID);
}
public void setNotificationBadge(boolean badge) {
notificationTabIcon.setImageResource(badge
? R.drawable.ic_fluent_alert_28_selector_badged
@@ -387,4 +364,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
setNotificationBadge(false);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragmentForTab(currentTab), assistContent);
}
}

View File

@@ -10,6 +10,7 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.assist.AssistContent;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
@@ -56,6 +57,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collection;
import java.util.HashMap;
@@ -73,7 +75,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, DomainDisplay, HasFab {
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent {
private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID;
@@ -108,7 +110,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
super.onCreate(savedInstanceState);
E.register(this);
accountID = getArguments().getString("account");
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
assert timelineDefinitions != null;
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
count = timelineDefinitions.size();
@@ -209,10 +211,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
if (fragments[position] instanceof BaseRecyclerFragment<?> page){
if(!page.loaded && !page.isDataLoading()) page.loadData();
}
//update recent app list url
if (fragments[position] instanceof DomainDisplay page)
DomainManager.getInstance().setCurrentDomain(page.getDomain());
}
});
@@ -297,14 +295,6 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}).exec(accountID);
}
@Override
public String getDomain() {
if (fragments[pager.getCurrentItem()] instanceof DomainDisplay page) {
return page.getDomain();
}
return DomainDisplay.super.getDomain();
}
private void onFabClick(View v){
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
l.onFabClick(v);
@@ -722,6 +712,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return fab;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent);
}
private class HomePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder> {
@NonNull
@Override

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -291,4 +292,9 @@ public class HomeTimelineFragment extends StatusListFragment {
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/").build();
}
}

View File

@@ -1,13 +1,13 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import androidx.annotation.Nullable;
@@ -168,4 +168,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/lists/" + listID).build();
}
}

View File

@@ -1,263 +0,0 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.CreateList;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.ListTimelineEditor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListTimelinesFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop {
private String accountId;
private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
private final HashMap<String, Boolean> userInList = new HashMap<>();
private ListsAdapter adapter;
public ListTimelinesFragment() {
super(10);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args=getArguments();
accountId=args.getString("account");
setHasOptionsMenu(true);
E.register(this);
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
String profileDisplayUsername = args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
} else {
setTitle(R.string.sk_your_lists);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.create) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_create_list_title)
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) ->
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountId)
)
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
}
return true;
}
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountId);
}
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
if (getActivity() == null) return;
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
if (getActivity() == null) return;
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountId);
}
})
.exec(accountId);
}
@Subscribe
public void onListDeletedEvent(ListDeletedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
data.remove(i);
adapter.notifyItemRemoved(i);
break;
}
}
}
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
item.title = event.title;
item.repliesPolicy = event.repliesPolicy;
adapter.notifyItemChanged(i);
break;
}
}
}
@Override
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
return adapter = new ListsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onClick() {
Bundle args=new Bundle();
args.putString("account", accountId);
args.putString("listID", item.id);
args.putString("listTitle", item.title);
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
}
}
}

View File

@@ -0,0 +1,270 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
import org.joinmastodon.android.api.requests.lists.CreateList;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.ListTimelineEditor;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListsFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
private final HashMap<String, Boolean> userInList = new HashMap<>();
private ListsAdapter adapter;
public ListsFragment() {
super(10);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
accountID = args.getString("account");
setHasOptionsMenu(true);
E.register(this);
if(args.containsKey("profileAccount")){
profileAccountId=args.getString("profileAccount");
String profileDisplayUsername = args.getString("profileDisplayUsername");
setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername));
} else {
setTitle(R.string.sk_your_lists);
}
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_list, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.create) {
ListTimelineEditor editor = new ListTimelineEditor(getContext());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_create_list_title)
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
.setView(editor)
.setPositiveButton(R.string.sk_create, (d, which) ->
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
@Override
public void onSuccess(ListTimeline list) {
data.add(0, list);
adapter.notifyItemRangeInserted(0, 1);
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID)
)
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
}
return true;
}
private void saveListMembership(String listId, boolean isMember) {
userInList.put(listId, isMember);
List<String> accountIdList = Collections.singletonList(profileAccountId);
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
req.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o) {}
@Override
public void onError(ErrorResponse error) {
error.showToast(getContext());
}
}).exec(accountID);
}
@Override
protected void doLoadData(int offset, int count){
userInListBefore.clear();
userInList.clear();
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<ListTimeline> lists) {
if (getActivity() == null) return;
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
userInList.putAll(userInListBefore);
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
if (profileAccountId == null) return;
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
@Override
public void onSuccess(List<ListTimeline> allLists) {
if (getActivity() == null) return;
List<ListTimeline> newLists = new ArrayList<>();
for (ListTimeline l : allLists) {
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
if (!userInListBefore.containsKey(l.id)) {
userInListBefore.put(l.id, false);
}
}
userInList.putAll(userInListBefore);
onDataLoaded(newLists, false);
}
}).exec(accountID);
}
})
.exec(accountID);
}
@Subscribe
public void onListDeletedEvent(ListDeletedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
data.remove(i);
adapter.notifyItemRemoved(i);
break;
}
}
}
@Subscribe
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
for (int i = 0; i < data.size(); i++) {
ListTimeline item = data.get(i);
if (item.id.equals(event.id)) {
item.title = event.title;
item.repliesPolicy = event.repliesPolicy;
adapter.notifyItemChanged(i);
break;
}
}
}
@Override
protected RecyclerView.Adapter<ListViewHolder> getAdapter() {
return adapter = new ListsAdapter();
}
@Override
public void scrollToTop() {
smoothScrollRecyclerViewToTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path("/lists").build();
}
private class ListsAdapter extends RecyclerView.Adapter<ListViewHolder>{
@NonNull
@Override
public ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ListViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ListViewHolder holder, int position) {
holder.bind(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
}
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
private final TextView title;
private final CheckBox listToggle;
public ListViewHolder(){
super(getActivity(), R.layout.item_text, list);
title=findViewById(R.id.title);
listToggle=findViewById(R.id.list_toggle);
}
@Override
public void onBind(ListTimeline item) {
title.setText(item.title);
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
if (profileAccountId != null) {
Boolean checked = userInList.get(item.id);
listToggle.setVisibility(View.VISIBLE);
listToggle.setChecked(userInList.containsKey(item.id) && checked != null && checked);
listToggle.setOnClickListener(this::onClickToggle);
} else {
listToggle.setVisibility(View.GONE);
}
}
private void onClickToggle(View view) {
saveListMembership(item.id, listToggle.isChecked());
}
@Override
public void onClick() {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("listID", item.id);
args.putString("listTitle", item.title);
if (item.repliesPolicy != null) args.putInt("repliesPolicy", item.repliesPolicy.ordinal());
Nav.go(getActivity(), ListTimelineFragment.class, args);
}
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -13,6 +14,12 @@ import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
@@ -24,12 +31,7 @@ import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -37,7 +39,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.V;
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, DomainDisplay{
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
private TabLayout tabLayout;
private ViewPager2 pager;
@@ -47,12 +49,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
private NotificationsListFragment allNotificationsFragment, mentionsFragment, postsFragment;
private String accountID;
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/notifications";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -107,6 +103,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
UiUtils.reduceSwipeSensitivity(pager);
tabViews=new FrameLayout[3];
for(int i=0;i<tabViews.length;i++){
@@ -124,6 +121,18 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
scrollToTop();
}
});
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
@@ -145,20 +154,17 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
args.putBoolean("noAutoLoad", true);
allNotificationsFragment=new NotificationsListFragment();
allNotificationsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyMentions", true);
args.putBoolean("noAutoLoad", true);
mentionsFragment=new NotificationsListFragment();
mentionsFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("onlyPosts", true);
args.putBoolean("noAutoLoad", true);
postsFragment=new NotificationsListFragment();
postsFragment.setArguments(args);
@@ -190,6 +196,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
@Override
public void onSuccess(HeaderPaginationList<Account> accounts) {
if (getActivity() == null) return;
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
}
@@ -228,6 +235,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
protected void updateToolbar(){
super.updateToolbar();
getToolbar().setOutlineProvider(null);
getToolbar().setOnClickListener(v->scrollToTop());
}
private NotificationsListFragment getFragmentForPage(int page){
@@ -239,6 +247,11 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
};
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
@@ -263,4 +276,4 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
return position;
}
}
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -10,6 +11,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
@@ -18,6 +20,8 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
@@ -53,11 +57,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
return false;
}
@Override
public String getDomain() {
return super.getDomain() + "/notifications";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -156,13 +155,17 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
loadRelationships(needRelationships);
maxID=result.maxID;
if(offset == 0 && !result.items.isEmpty() && !result.isFromCache() && AccountSessionManager.getInstance().getAccount(accountID).markers != null && AccountSessionManager.getInstance().getAccount(accountID).markers.notifications != null){
Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){
E.post(new AllNotificationsSeenEvent());
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
if (AccountSessionManager.getInstance().getAccount(accountID).markers != null)
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
if (isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
}
}
});
@@ -182,11 +185,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading){
refreshing=true;
loadData();
}
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
// loadData();
}
@Override
@@ -272,4 +272,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()
? "/users/" + getSession().self.username + "/interactions"
: "/notifications").build();
}
}

View File

@@ -21,7 +21,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment impl
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
}
@Override

View File

@@ -6,6 +6,7 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
@@ -67,6 +68,7 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.SimpleViewHolder;
@@ -83,6 +85,7 @@ import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.LinkedTextView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.time.LocalDateTime;
@@ -93,9 +96,13 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -116,7 +123,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab{
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@@ -146,6 +153,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private String note;
private Account account;
private String accountID;
private String domain;
private Relationship relationship;
private int statusBarHeight;
private boolean isOwnProfile;
@@ -160,7 +168,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
private static final int MAX_FIELDS=4;
private int maxFields = 4;
// from ProfileAboutFragment
public UsableRecyclerView list;
@@ -181,6 +189,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setRetainInstance(true);
accountID=getArguments().getString("account");
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
@@ -188,6 +197,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
loaded=true;
if(!isOwnProfile)
loadRelationship();
else if (isInstanceAkkoma() && getInstance().isPresent())
maxFields = getInstance().get().pleroma.metadata.fieldsLimits.maxFields;
}else{
profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false))
@@ -206,14 +217,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setHasOptionsMenu(true);
}
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden) {
DomainManager.getInstance().setCurrentDomain(account.url);
}
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View content=inflater.inflate(R.layout.fragment_profile, container, false);
@@ -396,7 +399,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setOnLongClickListener(v->{
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
usernameString+="@"+domain;
}
UiUtils.copyText(username, '@'+usernameString);
return true;
@@ -469,11 +472,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onRefresh(){
if(isInEditMode){
refreshing=false;
refreshLayout.setRefreshing(false);
return;
}
if(refreshing)
return;
refreshing=true;
@@ -577,12 +575,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private void bindHeaderView(){
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
if((GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic) != null){
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
}
if((GlobalUserPreferences.playGifs ? account.header : account.headerStatic) != null) {
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
}
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb);
@@ -610,7 +604,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
ssb.append(domain);
}
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
@@ -633,7 +627,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : ""));
}
CharSequence parsedBio = null;
if(account.note != null){
@@ -645,8 +639,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bio.setVisibility(View.VISIBLE);
bio.setText(parsedBio);
}
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
@@ -825,7 +817,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
args.putString("profileAccount", profileAccountID);
args.putString("profileDisplayUsername", account.getDisplayUsername());
}
Nav.go(getActivity(), ListTimelinesFragment.class, args);
Nav.go(getActivity(), ListsFragment.class, args);
}else if(id==R.id.followed_hashtags){
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -878,7 +870,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
// aboutFragment.setNote(relationship.note, accountID, profileAccountID);
}
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
DomainManager.getInstance().setCurrentDomain(account.url);
}
public ImageButton getFab() {
@@ -1215,11 +1206,6 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
scrollView.smoothScrollTo(0, 0);
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
}
private void onFollowersOrFollowingClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -1284,6 +1270,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if (adapter != null) adapter.notifyDataSetChanged();
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(account.url);
}
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
public MetadataAdapter(){
super(imgLoader);
@@ -1314,7 +1315,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public int getItemCount(){
if(isInEditMode){
int size=metadataListData.size();
if(size<MAX_FIELDS)
if(size<maxFields)
size++;
return size;
}
@@ -1448,7 +1449,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onClick(){
metadataListData.add(new AccountField());
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
if(metadataListData.size()==maxFields){ // replace this row with new row
adapter.notifyItemChanged(metadataListData.size()-1);
}else{
adapter.notifyItemInserted(metadataListData.size()-1);

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
@@ -182,4 +183,10 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
}
return null;
}
@Override
public Uri getWebUri(Uri.Builder base) {
// TODO: adapt when frontends finally implement a scheduled posts list
return null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -24,13 +25,13 @@ import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class StatusEditHistoryFragment extends StatusListFragment{
private String id;
private String id, url;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
id=getArguments().getString("id");
url=getArguments().getString("url");
loadData();
}
@@ -162,4 +163,9 @@ public class StatusEditHistoryFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() {
return null;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(url);
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.os.Bundle;
@@ -196,7 +197,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
}
@Override
public void onConfigurationChanged(Configuration newConfig){
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (getParentFragment() instanceof HomeTabFragment home) home.updateToolbarLogo();
}

View File

@@ -1,9 +1,13 @@
package org.joinmastodon.android.fragments;
import android.net.Uri;
import android.os.Bundle;
import android.util.Pair;
import android.view.View;
import org.joinmastodon.android.DomainManager;
import androidx.annotation.NonNull;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSession;
@@ -16,27 +20,51 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import me.grishka.appkit.api.SimpleCallback;
public class ThreadFragment extends StatusListFragment implements DomainDisplay{
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
protected Status mainStatus;
@Override
public String getDomain() {
return mainStatus.url;
}
/**
* lists the hierarchy of ancestors and descendants in a thread. level 0 = the main status.
* e.g.
* <pre>
* [0] ancestor: -2 ↰
* [1] ancestor: -1 ↰
* [2] main status: 0 ↰
* [3] descendant: 1 ↰
* [4] descendant: 2 ↰
* [5] descendant: 3
* [6] descendant: 1
* [7] descendant: 1 ↰
* [8] descendant: 2
* </pre>
* confused? good. /j
*/
private final List<Pair<String, Integer>> levels = new ArrayList<>();
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
@Override
public void onCreate(Bundle savedInstanceState){
@@ -48,21 +76,50 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
DomainManager.getInstance().setCurrentDomain(getDomain());
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=super.buildDisplayItems(s);
if(s.id.equals(mainStatus.id)){
for(StatusDisplayItem item:items){
// "what the fuck is a deque"? yes
// (it's just so the last-added item automatically comes first when looping over it)
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
// modifying hidden filtered items if status is displayed as a warning
List<StatusDisplayItem> itemsToModify =
(items.get(0) instanceof WarningFilteredStatusDisplayItem warning)
? warning.filteredItems
: items;
for(int i = 0; i < itemsToModify.size(); i++){
StatusDisplayItem item = itemsToModify.get(i);
NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id);
if (ancestryInfo != null) {
item.setAncestryInfo(
ancestryInfo.hasDescendantNeighbor(),
ancestryInfo.hasAncestoringNeighbor(),
s.id.equals(mainStatus.id),
ancestryInfo.getAncestoringNeighbor()
.map(ancestor -> ancestor.id.equals(mainStatus.id))
.orElse(false)
);
}
if (item instanceof ReblogOrReplyLineStatusDisplayItem &&
(!item.isDirectDescendant && item.hasAncestoringNeighbor)) {
deleteTheseItems.add(i);
}
if(s.id.equals(mainStatus.id)){
if(item instanceof TextStatusDisplayItem text)
text.textSelectable=true;
else if(item instanceof FooterStatusDisplayItem footer)
footer.hideCounts=true;
}
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
}
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
if(s.id.equals(mainStatus.id)) {
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus()));
}
return items;
}
@@ -76,36 +133,22 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
if (getActivity() == null) return;
if(refreshing){
data.clear();
ancestryMap.clear();
displayItems.clear();
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
}
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(instance.pleroma != null){
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:result.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=result.ancestors.size()-1; i >= 0; i--){
Status s=result.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
result.ancestors=result.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
result.descendants=getDescendantsOrdered(mainStatus.id,
result.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
// TODO: figure out how this code works
if(isInstanceAkkoma()) sortStatusContext(mainStatus, result);
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i);
}
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
@@ -114,7 +157,12 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
int count=displayItems.size();
if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
prependItems(result.ancestors, !refreshing);
int prependedCount = prependItems(result.ancestors, !refreshing);
if (prependedCount > 0 && displayItems.get(prependedCount) instanceof ReblogOrReplyLineStatusDisplayItem) {
displayItems.remove(prependedCount);
adapter.notifyItemRemoved(prependedCount);
count--;
}
dataLoaded();
if(refreshing){
refreshDone();
@@ -126,7 +174,61 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
.exec(accountID);
}
private List<Status> getDescendantsOrdered(String id, List<Status> statuses){
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
List<NeighborAncestryInfo> ancestry = new ArrayList<>();
List<Status> statuses = new ArrayList<>(context.ancestors);
statuses.add(mainStatus);
statuses.addAll(context.descendants);
int count = statuses.size();
for (int index = 0; index < count; index++) {
Status current = statuses.get(index);
NeighborAncestryInfo item = new NeighborAncestryInfo(current);
item.descendantNeighbor = Optional
.ofNullable(count > index + 1 ? statuses.get(index + 1) : null)
.filter(s -> s.inReplyToId.equals(current.id))
.orElse(null);
item.ancestoringNeighbor = Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null)
.filter(ancestor -> ancestor
.getDescendantNeighbor()
.map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id))
.orElse(false))
.flatMap(NeighborAncestryInfo::getStatus)
.orElse(null);
ancestry.add(item);
}
return ancestry;
}
public static void sortStatusContext(Status mainStatus, StatusContext context) {
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:context.descendants){
if(threadIds.contains(s.inReplyToId)){
threadIds.add(s.id);
}
}
threadIds.add(mainStatus.inReplyToId);
for(int i=context.ancestors.size()-1; i >= 0; i--){
Status s=context.ancestors.get(i);
if(s.inReplyToId != null && threadIds.contains(s.id)){
threadIds.add(s.inReplyToId);
}
}
context.ancestors=context.ancestors.stream().filter(s -> threadIds.contains(s.id)).collect(Collectors.toList());
context.descendants=getDescendantsOrdered(mainStatus.id,
context.descendants.stream()
.filter(s -> threadIds.contains(s.id))
.collect(Collectors.toList()));
}
private static List<Status> getDescendantsOrdered(String id, List<Status> statuses){
List<Status> out=new ArrayList<>();
for(Status s:getDirectDescendants(id, statuses)){
out.add(s);
@@ -138,7 +240,7 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
return out;
}
private List<Status> getDirectDescendants(String id, List<Status> statuses){
private static List<Status> getDirectDescendants(String id, List<Status> statuses){
return statuses.stream()
.filter(s -> s.inReplyToId.equals(id))
.collect(Collectors.toList());
@@ -195,4 +297,52 @@ public class ThreadFragment extends StatusListFragment implements DomainDisplay{
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.THREAD;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return Uri.parse(mainStatus.url);
}
public static class NeighborAncestryInfo {
protected Status status, descendantNeighbor, ancestoringNeighbor;
public NeighborAncestryInfo(@NonNull Status status) {
this.status = status;
}
public Optional<Status> getStatus() {
return Optional.ofNullable(status);
}
public Optional<Status> getDescendantNeighbor() {
return Optional.ofNullable(descendantNeighbor);
}
public Optional<Status> getAncestoringNeighbor() {
return Optional.ofNullable(ancestoringNeighbor);
}
public boolean hasDescendantNeighbor() {
return getDescendantNeighbor().isPresent();
}
public boolean hasAncestoringNeighbor() {
return getAncestoringNeighbor().isPresent();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NeighborAncestryInfo that = (NeighborAncestryInfo) o;
return status.equals(that.status)
&& Objects.equals(descendantNeighbor, that.descendantNeighbor)
&& Objects.equals(ancestoringNeighbor, that.ancestoringNeighbor);
}
@Override
public int hashCode() {
return Objects.hash(status, descendantNeighbor, ancestoringNeighbor);
}
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.model.Account;
@@ -14,4 +15,11 @@ public abstract class AccountRelatedAccountListFragment extends PaginatedAccount
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setTitle("@"+account.acct);
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()
? "/users/" + account.id
: '@' + account.acct).build();
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.account_list;
import android.app.ProgressDialog;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
@@ -23,7 +24,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
@@ -34,6 +36,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
@@ -57,7 +60,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> {
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri {
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@@ -175,6 +178,16 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super.onApplyWindowInsets(insets);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@@ -406,7 +419,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
args.putString("account", accountID);
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListTimelinesFragment.class, args);
Nav.go(getActivity(), ListsFragment.class, args);
}
return true;
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@@ -12,7 +13,6 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
targetAccount = account;
setSubtitle(getResources().getQuantityString(R.plurals.x_followers, (int)(account.followersCount%1000), account.followersCount));
}
@@ -22,7 +22,8 @@ public class FollowerListFragment extends AccountRelatedAccountListFragment{
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count){
return new GetAccountFollowers(id, maxID, count);
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath(isInstanceAkkoma() ? "#followers" : "/followers").build();
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@@ -12,7 +13,6 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
targetAccount = account;
setSubtitle(getResources().getQuantityString(R.plurals.x_following, (int)(account.followingCount%1000), account.followingCount));
}
@@ -22,7 +22,8 @@ public class FollowingListFragment extends AccountRelatedAccountListFragment{
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count){
return new GetAccountFollowing(id, maxID, count);
public Uri getWebUri(Uri.Builder base) {
return super.getWebUri(base).buildUpon()
.appendPath(isInstanceAkkoma() ? "#followees" : "/following").build();
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@@ -19,6 +20,14 @@ public class StatusFavoritesListFragment extends StatusRelatedAccountListFragmen
return new GetStatusFavorites(status.id, maxID, count);
}
@Override
public Uri getWebUri(Uri.Builder base) {
Uri statusUri = super.getWebUri(base);
return isInstanceAkkoma()
? statusUri
: statusUri.buildUpon().appendPath("favourites").build();
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count) {
return null;

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.R;
@@ -19,6 +20,14 @@ public class StatusReblogsListFragment extends StatusRelatedAccountListFragment{
return new GetStatusReblogs(status.id, maxID, count);
}
@Override
public Uri getWebUri(Uri.Builder base) {
Uri statusUri = super.getWebUri(base);
return isInstanceAkkoma()
? statusUri
: statusUri.buildUpon().appendPath("reblogs").build();
}
@Override
public HeaderPaginationRequest<Account> onCreateRemoteRequest(String id, String maxID, int count) {
return null;

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import org.joinmastodon.android.model.Status;
@@ -18,4 +19,13 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
protected boolean hasSubtitle(){
return false;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base
.encodedPath(isInstanceAkkoma()
? "/notice/" + status.id
: '@' + status.account.acct + '/' + status.id)
.build();
}
}

View File

@@ -0,0 +1,61 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class BubbleTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE);
private String maxID;
@Override
protected boolean wantsComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? base.path("/main/bubble").build() : null;
}
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
@@ -15,7 +16,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
@@ -28,6 +28,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.Collections;
@@ -51,7 +52,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, DomainDisplay {
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@@ -60,11 +61,6 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
super(20);
}
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/explore/suggestions";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -157,6 +153,16 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
return isRecyclerViewOnTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/suggestions").build();
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.app.FragmentTransaction;
import android.os.Build;
import android.os.Bundle;
@@ -22,13 +23,15 @@ import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.DomainManager;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.fragments.ListTimelinesFragment;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -40,7 +43,7 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, DomainDisplay {
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent {
private TabLayout tabLayout;
private ViewPager2 pager;
@@ -53,30 +56,14 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private ProgressBar searchProgress;
private DiscoverPostsFragment postsFragment;
private TrendingHashtagsFragment hashtagsFragment;
private DiscoverHashtagsFragment hashtagsFragment;
private DiscoverNewsFragment newsFragment;
private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment;
private LocalTimelineFragment localTimelineFragment;
private FederatedTimelineFragment federatedTimelineFragment;
private ListTimelinesFragment listTimelinesFragment;
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
// private final boolean noFederated = !GlobalUserPreferences.showFederatedTimeline;
@Override
public String getDomain() {
if (searchActive) {
return searchFragment.getDomain();
}
if (tabViews[tabLayout.getSelectedTabPosition()] instanceof DomainDisplay page) {
return page.getDomain();
}
return DomainDisplay.super.getDomain() + "/explore";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -94,22 +81,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
// tabViews=new FrameLayout[noFederated ? 5 : 6];
tabViews=new FrameLayout[4];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
/// int switchIndex = noFederated && i > 0 ? i + 1 : i;
// tabView.setId(switch(switchIndex){
// case 0 -> R.id.discover_local_timeline;
// case 1 -> R.id.discover_federated_timeline;
// case 2 -> R.id.discover_hashtags;
// case 3 -> R.id.discover_posts;
// case 4 -> R.id.discover_news;
// case 5 -> R.id.discover_users;
// default -> throw new IllegalStateException("Unexpected value: "+switchIndex);
// });
tabView.setId(switch(i){
case 0 -> R.id.discover_hashtags;
case 1 -> R.id.discover_posts;
@@ -117,7 +91,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 3 -> R.id.discover_users;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
tabViews[i]=tabView;
@@ -140,13 +113,10 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
if (_page instanceof DomainDisplay display)
DomainManager.getInstance().setCurrentDomain(display.getDomain());
}
});
if(localTimelineFragment==null || hashtagsFragment==null){
if(hashtagsFragment==null){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
@@ -154,7 +124,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment=new DiscoverPostsFragment();
postsFragment.setArguments(args);
hashtagsFragment=new TrendingHashtagsFragment();
hashtagsFragment=new DiscoverHashtagsFragment();
hashtagsFragment.setArguments(args);
newsFragment=new DiscoverNewsFragment();
@@ -163,27 +133,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
accountsFragment=new DiscoverAccountsFragment();
accountsFragment.setArguments(args);
localTimelineFragment=new LocalTimelineFragment();
localTimelineFragment.setArguments(args);
// listTimelinesFragment=new ListTimelinesFragment();
// listTimelinesFragment.setArguments(args);
//
// FragmentTransaction transaction = getChildFragmentManager().beginTransaction()
// .add(R.id.discover_posts, postsFragment)
// .add(R.id.discover_local_timeline, localTimelineFragment)
// .add(R.id.discover_hashtags, hashtagsFragment)
// .add(R.id.discover_news, newsFragment)
// .add(R.id.discover_users, accountsFragment)
// .add(R.id.discover_lists, listTimelinesFragment);
//
// if (!noFederated) {
// federatedTimelineFragment=new FederatedTimelineFragment();
// federatedTimelineFragment.setArguments(args);
// transaction.add(R.id.discover_federated_timeline, federatedTimelineFragment);
// }
//
// transaction.commit();
getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment)
@@ -196,21 +145,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
// if (noFederated && position > 0) position++;
// tab.setText(switch(position){
// case 0 -> R.string.local_timeline;
// case 1 -> R.string.sk_federated_timeline;
// case 2 -> R.string.sk_list_timelines;
// case 3 -> R.string.hashtags;
// case 4 -> R.string.posts;
// case 5 -> R.string.news;
// case 6 -> R.string.for_you;
//
// default -> throw new IllegalStateException("Unexpected value: "+position);
// });
tab.setText(switch(position){
case 0 -> R.string.hashtags;
case 1 -> R.string.posts;
@@ -224,9 +158,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayoutMediator.attach();
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
DomainManager.getInstance().setCurrentDomain(getDomain());
}
public void onTabSelected(TabLayout.Tab tab){}
@Override
public void onTabUnselected(TabLayout.Tab tab){}
@@ -303,20 +235,25 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
@Override
public boolean isScrolledToTop() {
if(!searchActive){
return ((ScrollableToTop)getFragmentForPage(pager.getCurrentItem())).isScrolledToTop();
}else{
return searchFragment.isScrolledToTop();
}
public boolean isOnTop() {
return searchActive ? searchFragment.isOnTop()
: ((IsOnTop)getFragmentForPage(pager.getCurrentItem())).isOnTop();
}
public void onSelect() {
if (isOnTop()) selectSearch();
else scrollToTop();
}
public void selectSearch() {
searchEdit.requestFocus();
onSearchEditFocusChanged(searchEdit, true);
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
}
public void loadData(){
if(hashtagsFragment!=null && !hashtagsFragment.loaded && !hashtagsFragment.dataLoading)
hashtagsFragment.loadData();
// if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
// localTimelineFragment.loadData();
}
private void onSearchEditFocusChanged(View v, boolean hasFocus){
@@ -342,6 +279,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
if (getArguments().getBoolean("disableDiscover"))
((HomeFragment) getParentFragment()).onBackPressed();
}
@Override
@@ -351,19 +290,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
private Fragment getFragmentForPage(int page){
// if (noFederated && page > 0) page++;
// return switch(page){
// case 0 -> localTimelineFragment;
// case 1 -> federatedTimelineFragment;
// case 2 -> hashtagsFragment;
// case 3 -> postsFragment;
// case 4 -> newsFragment;
// case 5 -> accountsFragment;
// case 6 -> listTimelinesFragment;
// default -> throw new IllegalStateException("Unexpected value: "+page);
// };
return switch(page){
case 0 -> hashtagsFragment;
case 1 -> postsFragment;
@@ -401,6 +327,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(searchActive
? searchFragment
: getFragmentForPage(pager.getCurrentItem()), assistContent);
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@@ -1,5 +1,9 @@
package org.joinmastodon.android.fragments.discover;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@@ -7,7 +11,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
@@ -16,29 +19,24 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, DomainDisplay {
public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public TrendingHashtagsFragment(){
public DiscoverHashtagsFragment(){
super(10);
}
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/explore/tags";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -76,13 +74,18 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@@ -117,14 +120,19 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
@Override
public void onBind(Hashtag item){
title.setText('#'+item.name);
int numPeople = 0;
if(item.history != null){
numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
chart.setData(item.history);
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
subtitle.setText(getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
chart.setData(item.history);
}
@Override

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -10,7 +11,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.DomainDisplay;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
@@ -20,6 +20,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collections;
import java.util.List;
@@ -28,7 +29,6 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
@@ -37,7 +37,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop, IsOnTop, DomainDisplay {
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
@@ -46,11 +46,6 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
super(10);
}
@Override
public String getDomain() {
return DomainDisplay.super.getDomain() + "/explore/links";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -91,13 +86,18 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
}
@Override
public boolean isScrolledToTop() {
return list.getChildAt(0).getTop() == 0;
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/links").build();
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -48,9 +49,13 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
return isRecyclerViewOnTop(list);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/posts").build();
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@@ -25,12 +26,6 @@ public class FederatedTimelineFragment extends StatusListFragment {
return true;
}
@Override
public String getDomain() {
return super.getDomain() + "/public";
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
@@ -50,11 +45,16 @@ public class FederatedTimelineFragment extends StatusListFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
// bannerHelper.maybeAddBanner(contentWrap);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/main/all" : "/public").build();
}
}

View File

@@ -1,9 +1,11 @@
package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
@@ -24,11 +26,6 @@ public class LocalTimelineFragment extends StatusListFragment {
return true;
}
@Override
public String getDomain() {
return super.getDomain() + "/public/local";
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
@@ -55,4 +52,9 @@ public class LocalTimelineFragment extends StatusListFragment {
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma() ? "/main/public" : "/public/local").build();
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Activity;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
@@ -11,6 +12,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
@@ -42,7 +44,7 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
public class SearchFragment extends BaseStatusListFragment<SearchResult> implements IsOnTop {
private String currentQuery;
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
@@ -57,11 +59,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
setLayout(R.layout.fragment_search);
}
@Override
public String getDomain() {
return super.getDomain() + "/search";
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -190,7 +187,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return;
}
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
boolean recent=isInRecentMode();
boolean recent=isInRecentMode() && !displayItems.isEmpty();
if(recent!=headerAdapter.isVisible())
headerAdapter.setVisible(recent);
imgLoader.forceUpdateImages();
@@ -316,6 +313,19 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
}
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public Uri getWebUri(Uri.Builder base) {
Uri.Builder searchUri = base.path("/search");
return isInstanceAkkoma()
? searchUri.appendQueryParameter("query", currentQuery).build()
: searchUri.build();
}
@FunctionalInterface
public interface ProgressVisibilityListener{
void onProgressVisibilityChanged(boolean visible);

View File

@@ -23,7 +23,8 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -69,7 +70,7 @@ public class AccountActivationFragment extends ToolbarFragment{
openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsMainFragment.class, args);
Nav.go(getActivity(), SettingsFragment.class, args);
return true;
});
resendBtn=view.findViewById(R.id.btn_resend);
@@ -89,15 +90,8 @@ public class AccountActivationFragment 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));
}
// @Override
protected void onUpdateToolbar(){
// super.onUpdateToolbar();
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
@@ -109,10 +103,7 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override
public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity(), true, true, false, accountSession -> {
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}).show();
new AccountSwitcherSheet(getActivity(), null).show();
}
@Override

View File

@@ -2,7 +2,9 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.assist.AssistContent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
@@ -24,6 +26,7 @@ import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import androidx.annotation.NonNull;
@@ -38,7 +41,7 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends ToolbarFragment{
public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAssistContent {
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@@ -130,6 +133,15 @@ public class InstanceRulesFragment extends ToolbarFragment{
}
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(new Uri.Builder()
.scheme("https")
.authority(instance.normalizedUri)
.path("/about")
.build());
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull

View File

@@ -5,6 +5,7 @@ import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseIntArray;
@@ -267,4 +268,11 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
protected Filter.FilterContext getFilterContext() {
return null;
}
@Override
public Uri getWebUri(Uri.Builder base) {
if (reportStatus != null) return Uri.parse(reportStatus.url);
if (reportAccount != null) return Uri.parse(reportAccount.url);
return null;
}
}

View File

@@ -11,6 +11,7 @@ import java.net.IDN;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Parcel
public class Instance extends BaseModel{
@@ -86,6 +87,11 @@ public class Instance extends BaseModel{
public Pleroma pleroma;
public PleromaPollLimits pollLimits;
/** like uri, but always without scheme and trailing slash */
public transient String normalizedUri;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
@@ -95,6 +101,10 @@ public class Instance extends BaseModel{
rules=Collections.emptyList();
if(shortDescription==null)
shortDescription="";
// akkoma says uri is "https://example.social" while just "example.social" on mastodon
normalizedUri = uri
.replaceFirst("^https://", "")
.replaceFirst("/$", "");
}
@Override
@@ -134,6 +144,26 @@ public class Instance extends BaseModel{
return ci;
}
public boolean isAkkoma() {
return pleroma != null;
}
public boolean hasFeature(Feature feature) {
Optional<List<String>> pleromaFeatures = Optional.ofNullable(pleroma)
.map(p -> p.metadata)
.map(m -> m.features);
return switch (feature) {
case BUBBLE_TIMELINE -> pleromaFeatures
.map(f -> f.contains("bubble_timeline"))
.orElse(false);
};
}
public enum Feature {
BUBBLE_TIMELINE
}
@Parcel
public static class Rule{
public String id;
@@ -198,6 +228,28 @@ public class Instance extends BaseModel{
@Parcel
public static class Pleroma extends BaseModel {
// metadata etc
public Pleroma.Metadata metadata;
@Parcel
public static class Metadata {
public List<String> features;
public Pleroma.Metadata.FieldsLimits fieldsLimits;
@Parcel
public static class FieldsLimits {
public int maxFields;
public int maxRemoteFields;
public int nameLength;
public int valueLength;
}
}
}
@Parcel
public static class PleromaPollLimits {
public int maxExpiration;
public int maxOptionChars;
public int maxOptions;
public int minExpiration;
}
}

View File

@@ -11,15 +11,19 @@ import androidx.annotation.StringRes;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.CustomLocalTimelineFragment;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.discover.BubbleTimelineFragment;
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class TimelineDefinition {
private TimelineType type;
@@ -65,6 +69,14 @@ public class TimelineDefinition {
this.type = type;
}
public boolean isCompatible(AccountSession session) {
return true;
}
public boolean wantsDefault(AccountSession session) {
return true;
}
public String getTitle(Context ctx) {
return title != null ? title : getDefaultTitle(ctx);
}
@@ -85,6 +97,7 @@ public class TimelineDefinition {
case POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
case LIST -> listTitle;
case HASHTAG -> hashtagName;
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
case CUSTOM_LOCAL_TIMELINE -> domain;
};
}
@@ -98,6 +111,7 @@ public class TimelineDefinition {
case LIST -> Icon.LIST;
case HASHTAG -> Icon.HASHTAG;
case CUSTOM_LOCAL_TIMELINE -> Icon.CUSTOM_LOCAL_TIMELINE;
case BUBBLE -> Icon.BUBBLE;
};
}
@@ -109,6 +123,7 @@ public class TimelineDefinition {
case LIST -> new ListTimelineFragment();
case HASHTAG -> new HashtagTimelineFragment();
case POST_NOTIFICATIONS -> new NotificationsListFragment();
case BUBBLE -> new BubbleTimelineFragment();
case CUSTOM_LOCAL_TIMELINE -> new CustomLocalTimelineFragment();
};
}
@@ -172,7 +187,7 @@ public class TimelineDefinition {
return args;
}
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, CUSTOM_LOCAL_TIMELINE }
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, CUSTOM_LOCAL_TIMELINE, BUBBLE }
public enum Icon {
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
@@ -236,7 +251,8 @@ public class TimelineDefinition {
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
CUSTOM_LOCAL_TIMELINE(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true);
CUSTOM_LOCAL_TIMELINE(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);
public final int iconRes, nameRes;
public final boolean hidden;
@@ -256,14 +272,50 @@ public class TimelineDefinition {
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
@Override
public boolean isCompatible(AccountSession session) {
// still enabling the bubble timeline for all pleroma/akkoma instances since i know of
// at least one instance that supports it, but doesn't list "bubble_timeline"
return session.getInstance().map(Instance::isAkkoma).orElse(false);
}
public static final List<TimelineDefinition> DEFAULT_TIMELINES = BuildConfig.BUILD_TYPE.equals("playRelease")
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE.copy(),
LOCAL_TIMELINE.copy(),
FEDERATED_TIMELINE.copy(),
POSTS_TIMELINE.copy()
@Override
public boolean wantsDefault(AccountSession session) {
return session.getInstance()
.map(i -> i.hasFeature(Instance.Feature.BUBBLE_TIMELINE))
.orElse(false);
}
};
public static List<TimelineDefinition> getDefaultTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return DEFAULT_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session) && tl.wantsDefault(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
public static List<TimelineDefinition> getAllTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return ALL_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
private static final List<TimelineDefinition> DEFAULT_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
BUBBLE_TIMELINE,
FEDERATED_TIMELINE
);
private static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POSTS_TIMELINE,
BUBBLE_TIMELINE
);
}

View File

@@ -2,7 +2,8 @@ package org.joinmastodon.android.ui;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -13,24 +14,31 @@ import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
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.HomeFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -49,18 +57,26 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class AccountSwitcherSheet extends BottomSheet{
private final Activity activity;
private final HomeFragment fragment;
private final BiConsumer<String, Boolean> onClick;
private final boolean externalShare, openInApp;
private UsableRecyclerView list;
private List<WrappedAccount> accounts;
private ListImageLoaderWrapper imgLoader;
private final boolean logOutEnabled;
private final Consumer<AccountSession> onClick;
private AccountsAdapter accountsAdapter;
public AccountSwitcherSheet(@NonNull Activity activity, boolean logOutEnabled, boolean addAccountEnabled, boolean showOpenURL, Consumer<AccountSession> onClick){
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
this(activity, fragment, false, false, null);
}
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment, boolean externalShare, boolean openInApp, BiConsumer<String, Boolean> onClick){
super(activity);
this.activity=activity;
this.logOutEnabled=logOutEnabled;
this.onClick=onClick;
this.fragment=fragment;
this.externalShare = externalShare;
this.openInApp = openInApp;
this.onClick = onClick;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity);
@@ -71,61 +87,59 @@ public class AccountSwitcherSheet extends BottomSheet{
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
View handle=new View(activity);
handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
adapter.addAdapter(new AccountsAdapter());
handle.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(36)));
if(addAccountEnabled){
AccountViewHolder holder = new AccountViewHolder();
holder.more.setVisibility(View.GONE);
holder.currentIcon.setVisibility(View.GONE);
holder.display_name.setVisibility(View.GONE);
holder.display_add_account.setVisibility(View.VISIBLE);
holder.avatar.setScaleType(ImageView.ScaleType.CENTER);
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, () -> {
adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
if (externalShare) {
FrameLayout shareHeading = new FrameLayout(activity);
activity.getLayoutInflater().inflate(R.layout.item_external_share_heading, shareHeading);
((TextView) shareHeading.findViewById(R.id.title)).setText(openInApp
? R.string.sk_external_share_or_open_title
: R.string.sk_external_share_title);
adapter.addAdapter(new SingleViewRecyclerAdapter(shareHeading));
setOnDismissListener((d) -> activity.finish());
}
adapter.addAdapter(accountsAdapter = new AccountsAdapter());
if (!externalShare) {
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_fluent_add_24_regular), () -> {
Nav.go(activity, CustomWelcomeFragment.class, null);
dismiss();
}));
}
if(showOpenURL) {
AccountViewHolder holder = new AccountViewHolder();
holder.more.setVisibility(View.GONE);
holder.currentIcon.setVisibility(View.GONE);
holder.display_name.setVisibility(View.VISIBLE);
holder.display_add_account.setVisibility(View.VISIBLE);
holder.display_add_account.setText(R.string.mo_share_open_url);
holder.avatar.setScaleType(ImageView.ScaleType.CENTER);
holder.avatar.setImageResource(R.drawable.ic_fluent_open_24_regular);
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary)));
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, () -> {
onClick.accept(null);
dismiss();
}));
// disabled in megalodon
// adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.log_out_all_accounts, R.drawable.ic_fluent_person_arrow_right_24_filled), this::confirmLogOutAll));
}
list.setAdapter(adapter);
DividerItemDecoration divider=new DividerItemDecoration(activity, R.attr.colorPollVoted, .5f, 72, 16, DividerItemDecoration.NOT_FIRST);
divider.setDrawBelowLastItem(true);
list.addItemDecoration(divider);
FrameLayout content=new FrameLayout(activity);
content.setBackgroundResource(R.drawable.bg_bottom_sheet);
content.addView(list);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(activity, R.attr.colorM3Surface),
UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
}
private void confirmLogOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(activity)
.setTitle(R.string.log_out)
.setMessage(R.string.confirm_log_out)
.setMessage(activity.getString(R.string.confirm_log_out, session.getFullUsername()))
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
.setNegativeButton(R.string.cancel, null)
.show();
}
private void confirmLogOutAll(){
new M3AlertDialogBuilder(activity)
.setMessage(R.string.confirm_log_out_all_accounts)
.setPositiveButton(R.string.log_out, (dialog, which) -> logOutAll())
.setNegativeButton(R.string.cancel, null)
.show();
}
private void logOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
@@ -144,9 +158,55 @@ public class AccountSwitcherSheet extends BottomSheet{
.exec(accountID);
}
private void logOutAll(){
final ProgressDialog progress=new ProgressDialog(activity);
progress.setMessage(activity.getString(R.string.loading));
progress.setCancelable(false);
progress.show();
ArrayList<AccountSession> sessions=new ArrayList<>(AccountSessionManager.getInstance().getLoggedInAccounts());
for(AccountSession session:sessions){
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
AccountSessionManager.getInstance().removeAccount(session.getID());
sessions.remove(session);
if(sessions.isEmpty()){
progress.dismiss();
Nav.goClearingStack(activity, SplashFragment.class, null);
dismiss();
}
}
@Override
public void onError(ErrorResponse error){
AccountSessionManager.getInstance().removeAccount(session.getID());
sessions.remove(session);
if(sessions.isEmpty()){
progress.dismiss();
Nav.goClearingStack(activity, SplashFragment.class, null);
dismiss();
}
}
})
.exec(session.getID());
}
}
private void onLoggedOut(String accountID){
AccountSessionManager.getInstance().removeAccount(accountID);
dismiss();
String activeAccountID = fragment != null
? fragment.getAccountID()
: AccountSessionManager.getInstance().getLastActiveAccountID();
if (accountID.equals(activeAccountID)) {
activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class));
} else {
accounts.stream().filter(w -> accountID.equals(w.session.getID())).findAny().ifPresent(w -> {
accountsAdapter.notifyItemRemoved(accounts.indexOf(w));
accounts.remove(w);
});
}
}
@Override
@@ -164,6 +224,13 @@ public class AccountSwitcherSheet extends BottomSheet{
}
}
private View makeSimpleListItem(@StringRes int title, @DrawableRes int icon){
TextView tv=(TextView) activity.getLayoutInflater().inflate(R.layout.item_text_with_icon, list, false);
tv.setText(title);
tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, 0, 0, 0);
return tv;
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@@ -197,55 +264,42 @@ public class AccountSwitcherSheet extends BottomSheet{
}
}
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name;
private final TextView display_name;
private final TextView display_add_account;
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name, username;
private final ImageView avatar;
private final ImageButton more;
private final View currentIcon;
private final PopupMenu menu;
private final CheckableRelativeLayout view;
private final View radioButton, extraBtnWrap;
private final ImageButton extraBtn;
public AccountViewHolder(){
super(activity, R.layout.item_account_switcher, list);
name=findViewById(R.id.name);
display_name=findViewById(R.id.display_name);
display_add_account=findViewById(R.id.add_account);
username=findViewById(R.id.username);
radioButton=findViewById(R.id.radiobtn);
radioButton.setBackground(new RadioButton(activity).getButtonDrawable());
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
currentIcon=findViewById(R.id.current);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setOutlineProvider(OutlineProviders.roundedRect(OutlineProviders.RADIUS_MEDIUM));
avatar.setClipToOutline(true);
menu=new PopupMenu(activity, more);
menu.inflate(R.menu.account_switcher);
menu.setOnMenuItemClickListener(item1 -> {
confirmLogOut(item.getID());
return true;
});
more.setOnClickListener(v->menu.show());
view=(CheckableRelativeLayout) itemView;
extraBtnWrap = findViewById(R.id.extra_btn_wrap);
extraBtn = findViewById(R.id.extra_btn);
extraBtn.setOnClickListener(this::onExtraBtnClick);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(AccountSession item){
display_name.setText(item.self.displayName);
name.setText("@"+item.self.username+"@"+item.domain);
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){
more.setVisibility(View.GONE);
currentIcon.setVisibility(View.VISIBLE);
}else{
more.setVisibility(View.VISIBLE);
currentIcon.setVisibility(View.GONE);
name.setText(item.self.displayName);
username.setText(item.getFullUsername());
radioButton.setVisibility(externalShare ? View.GONE : View.VISIBLE);
extraBtnWrap.setVisibility(externalShare && openInApp ? View.VISIBLE : View.GONE);
if (externalShare) view.setCheckable(false);
else {
String accountId = fragment != null
? fragment.getAccountID()
: AccountSessionManager.getInstance().getLastActiveAccountID();
view.setChecked(accountId.equals(item.getID()));
}
if(!logOutEnabled){
more.setVisibility(View.GONE);
currentIcon.setVisibility(View.GONE);
}
menu.getMenu().findItem(R.id.log_out).setTitle(activity.getString(R.string.log_out_account, "@"+item.self.username));
UiUtils.enablePopupMenuIcons(activity, menu);
}
@Override
@@ -260,11 +314,31 @@ public class AccountSwitcherSheet extends BottomSheet{
setImage(index, null);
}
private void onExtraBtnClick(View view) {
setOnDismissListener(null);
dismiss();
onClick.accept(item.getID(), true);
}
@Override
public void onClick(){
setOnDismissListener(null);
if (onClick != null) {
dismiss();
onClick.accept(item.getID(), false);
return;
}
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
dismiss();
onClick.accept(AccountSessionManager.getInstance().getAccount(item.getID()));
activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class));
}
@Override
public boolean onLongClick(){
if (externalShare) return false;
confirmLogOut(item.getID());
return true;
}
}

View File

@@ -18,13 +18,15 @@ package org.joinmastodon.android.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.ViewPropertyAnimator;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.SimpleItemAnimator;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import java.util.ArrayList;
@@ -358,7 +360,14 @@ public class BetterItemAnimator extends SimpleItemAnimator{
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
float alpha = 0;
if (holder instanceof MediaGridStatusDisplayItem.Holder mediaItemHolder) {
if (mediaItemHolder.isSizeUpdating()) {
alpha = 1; // Image will flicker out and then in if alpha is 0
mediaItemHolder.sizeUpdated();
}
}
oldViewAnim.alpha(alpha).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchChangeStarting(changeInfo.oldHolder, true);

View File

@@ -8,7 +8,15 @@ import android.view.ViewOutlineProvider;
import me.grishka.appkit.utils.V;
public class OutlineProviders{
private static SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>();
private static final SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>();
private static final SparseArray<ViewOutlineProvider> topRoundedRects=new SparseArray<>();
private static final SparseArray<ViewOutlineProvider> endRoundedRects=new SparseArray<>();
public static final int RADIUS_XSMALL=4;
public static final int RADIUS_SMALL=8;
public static final int RADIUS_MEDIUM=12;
public static final int RADIUS_LARGE=16;
public static final int RADIUS_XLARGE=28;
private OutlineProviders(){
//no instance
@@ -21,6 +29,12 @@ public class OutlineProviders{
outline.setAlpha(view.getAlpha());
}
};
public static final ViewOutlineProvider OVAL=new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
};
public static ViewOutlineProvider roundedRect(int dp){
ViewOutlineProvider provider=roundedRects.get(dp);
@@ -31,6 +45,24 @@ public class OutlineProviders{
return provider;
}
public static ViewOutlineProvider topRoundedRect(int dp){
ViewOutlineProvider provider=topRoundedRects.get(dp);
if(provider!=null)
return provider;
provider=new TopRoundRectOutlineProvider(V.dp(dp));
topRoundedRects.put(dp, provider);
return provider;
}
public static ViewOutlineProvider endRoundedRect(int dp){
ViewOutlineProvider provider=endRoundedRects.get(dp);
if(provider!=null)
return provider;
provider=new EndRoundRectOutlineProvider(V.dp(dp));
endRoundedRects.put(dp, provider);
return provider;
}
private static class RoundRectOutlineProvider extends ViewOutlineProvider{
private final int radius;
@@ -43,4 +75,34 @@ public class OutlineProviders{
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
}
}
private static class TopRoundRectOutlineProvider extends ViewOutlineProvider{
private final int radius;
private TopRoundRectOutlineProvider(int radius){
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight()+radius, radius);
}
}
private static class EndRoundRectOutlineProvider extends ViewOutlineProvider{
private final int radius;
private EndRoundRectOutlineProvider(int radius){
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
if(view.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL){
outline.setRoundRect(-radius, 0, view.getWidth(), view.getHeight(), radius);
}else{
outline.setRoundRect(0, 0, view.getWidth()+radius, view.getHeight(), radius);
}
}
}
}

View File

@@ -139,6 +139,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
Bundle args=new Bundle();
args.putString("account", item.parentFragment.getAccountID());
args.putString("id", item.status.id);
args.putString("url", item.status.url);
Nav.go(item.parentFragment.getActivity(), StatusEditHistoryFragment.class, args);
}
}

View File

@@ -148,12 +148,27 @@ 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);
// in thread view, direct descendant posts display one direct reply to themselves,
// hence in that case displaying whether there is another reply
int compareTo = item.isMainStatus || !item.hasDescendantNeighbor ? 0 : 1;
reply.setSelected(item.status.repliesCount > compareTo);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.canBeBoosted(item.accountID));
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
int nextPos = getAbsoluteAdapterPosition() + 1;
boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos &&
item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem;
boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor &&
!nextIsWarning;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
params.setMargins(params.leftMargin, params.topMargin, params.rightMargin,
condenseBottom ? V.dp(-8) : 0);
itemView.requestLayout();
}
private void bindButton(TextView btn, long count){

View File

@@ -1,7 +1,9 @@
package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -25,6 +27,13 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
private final TextView title, subtitle;
private final HashtagChartView chart;
public static final RelativeLayout.LayoutParams
withHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
withoutHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
static {
withoutHistoryParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
}
public Holder(Context context, ViewGroup parent){
super(context, R.layout.item_trending_hashtag, parent);
@@ -37,14 +46,20 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public void onBind(HashtagStatusDisplayItem _item){
Hashtag item=_item.tag;
title.setText('#'+item.name);
int numPeople = 0;
if(item.history != null){
numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
chart.setData(item.history);
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;
subtitle.setText(_item.parentFragment.getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
chart.setData(item.history);
}
}
}

View File

@@ -1,11 +1,9 @@
package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
@@ -27,8 +25,6 @@ import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.StringRes;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
@@ -39,7 +35,7 @@ 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.ListTimelinesFragment;
import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
@@ -47,12 +43,10 @@ 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.ContentType;
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;
@@ -62,7 +56,6 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -290,7 +283,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
args.putString("account", item.parentFragment.getAccountID());
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(item.parentFragment.getActivity(), ListTimelinesFragment.class, args);
Nav.go(item.parentFragment.getActivity(), ListsFragment.class, args);
}
if(!item.status.filterRevealed){

View File

@@ -7,6 +7,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
@@ -17,7 +18,6 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
@@ -40,7 +40,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private static final String TAG="MediaGridDisplayItem";
private final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
private final List<Attachment> attachments;
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
@@ -98,6 +98,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private int altTextIndex=-1;
private Animator altTextAnimator;
private boolean sizeUpdating = false;
public Holder(Activity activity, ViewGroup parent){
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
wrapper=(FrameLayout)itemView;
@@ -126,6 +128,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
}
layout.removeAllViews();
controllers.clear();
int i=0;
for(Attachment att:item.attachments){
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
@@ -158,6 +161,19 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
@Override
public void setImage(int index, Drawable drawable){
Rect bounds=drawable.getBounds();
drawable.setBounds(bounds.left, bounds.top, bounds.left+drawable.getIntrinsicWidth(), bounds.top+drawable.getIntrinsicHeight());
if(item.attachments.get(index).meta==null){
Attachment.Metadata metadata = new Attachment.Metadata();
metadata.width=drawable.getIntrinsicWidth();
metadata.height=drawable.getIntrinsicHeight();
item.attachments.get(index).meta=metadata;
item.tiledLayout=PhotoLayoutHelper.processThumbs(item.attachments);
sizeUpdating = true;
item.parentFragment.onImageUpdated(this, index);
}
controllers.get(index).setImage(drawable);
}
@@ -314,5 +330,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
layout.setClipChildren(clip);
wrapper.setClipChildren(clip);
}
public boolean isSizeUpdating() {
return sizeUpdating;
}
public void sizeUpdated() {
sizeUpdating = false;
}
}
}

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -26,6 +27,7 @@ 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;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels;
@@ -45,6 +47,23 @@ public abstract class StatusDisplayItem{
public final BaseStatusListFragment parentFragment;
public boolean inset;
public int index;
public boolean
hasDescendantNeighbor = false,
hasAncestoringNeighbor = false,
isMainStatus = true,
isDirectDescendant = false;
public void setAncestryInfo(
boolean hasDescendantNeighbor,
boolean hasAncestoringNeighbor,
boolean isMainStatus,
boolean isDirectDescendant
) {
this.hasDescendantNeighbor = hasDescendantNeighbor;
this.hasAncestoringNeighbor = hasAncestoringNeighbor;
this.isMainStatus = isMainStatus;
this.isDirectDescendant = isDirectDescendant;
}
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
this.parentID=parentID;
@@ -163,6 +182,15 @@ public abstract class StatusDisplayItem{
items.add(replyLine);
}
if (statusForContent.quote != null) {
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">");
if (!hasQuoteInlineTag) {
String quoteUrl = statusForContent.quote.url;
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
statusForContent.content += quoteInline;
}
}
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null)
@@ -174,6 +202,12 @@ public abstract class StatusDisplayItem{
.filter(att->att.type.isImage() && !att.type.equals(Attachment.Type.UNKNOWN))
.collect(Collectors.toList());
if(!imageAttachments.isEmpty()){
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorAccentLightest);
for (Attachment att : imageAttachments) {
if (att.blurhashPlaceholder == null) {
att.blurhashPlaceholder = new ColorDrawable(color);
}
}
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
}

View File

@@ -65,6 +65,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
spoilerEmojiHelper.setText(parsedSpoilerText);
}
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
UiUtils.loadMaxWidth(parentFragment.getContext());
}
public void setTranslationShown(boolean translationShown) {
@@ -225,13 +226,33 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
// remove additional padding when (transparently padded) translate button is visible
int pos = getAbsoluteAdapterPosition();
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(),
(translateVisible &&
item.parentFragment.getDisplayItems().size() >= pos + 1 &&
item.parentFragment.getDisplayItems().get(pos + 1) instanceof FooterStatusDisplayItem)
? 0 : V.dp(12)
);
if (!GlobalUserPreferences.collapseLongPosts) {
textScrollView.setLayoutParams(wrapParams);
readMore.setVisibility(View.GONE);
}
// incredibly ugly workaround for https://github.com/sk22/megalodon/issues/520
// i am so, so sorry. FIXME
// attempts to use OnPreDrawListener, OnGlobalLayoutListener and .post have failed -
// the view didn't want to reliably update after calling .setVisibility etc :(
int width = parent.getWidth() != 0 ? parent.getWidth()
: item.parentFragment.getView().getWidth() != 0
? item.parentFragment.getView().getWidth()
: item.parentFragment.getParentFragment() != null && item.parentFragment.getParentFragment().getView().getWidth() != 0
? item.parentFragment.getParentFragment().getView().getWidth() // YIKES
: UiUtils.MAX_WIDTH;
text.measure(
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {

View File

@@ -38,6 +38,7 @@ public class DiscoverInfoBannerHelper{
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
case BUBBLE_TIMELINE -> R.string.sk_bubble_timeline_info_banner;
});
}
}
@@ -63,6 +64,7 @@ public class DiscoverInfoBannerHelper{
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POST_NOTIFICATIONS,
// ACCOUNTS
// ACCOUNTS,
BUBBLE_TIMELINE
}
}

View File

@@ -7,6 +7,7 @@ import static org.joinmastodon.android.GlobalUserPreferences.trueBlackTheme;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
@@ -102,6 +103,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
@@ -114,6 +116,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -141,12 +144,13 @@ import me.grishka.appkit.utils.V;
import okhttp3.MediaType;
public class UiUtils {
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
private static Handler mainHandler = new Handler(Looper.getMainLooper());
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
public static int MAX_WIDTH;
private UiUtils(){}
private UiUtils() {
}
public static void loadMaxWidth(Context ctx) {
if (MAX_WIDTH == 0) MAX_WIDTH = (int) ctx.getResources().getDimension(R.dimen.layout_max_width);
@@ -159,33 +163,33 @@ public class UiUtils {
.setShowTitle(true)
.build()
.launchUrl(context, Uri.parse(url));
}else{
} else {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}catch(ActivityNotFoundException x){
} catch (ActivityNotFoundException x) {
Toast.makeText(context, R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
}
}
public static String formatRelativeTimestamp(Context context, Instant instant){
long t=instant.toEpochMilli();
long now=System.currentTimeMillis();
long diff=now-t;
if(diff<1000L){
public static String formatRelativeTimestamp(Context context, Instant instant) {
long t = instant.toEpochMilli();
long now = System.currentTimeMillis();
long diff = now - t;
if (diff < 1000L) {
return context.getString(R.string.time_now);
}else if(diff<60_000L){
return context.getString(R.string.time_seconds, diff/1000L);
}else if(diff<3600_000L){
return context.getString(R.string.time_minutes, diff/60_000L);
}else if(diff<3600_000L*24L){
return context.getString(R.string.time_hours, diff/3600_000L);
}else{
int days=(int)(diff/(3600_000L*24L));
if(days>30){
ZonedDateTime dt=instant.atZone(ZoneId.systemDefault());
if(dt.getYear()==ZonedDateTime.now().getYear()){
} else if (diff < 60_000L) {
return context.getString(R.string.time_seconds, diff / 1000L);
} else if (diff < 3600_000L) {
return context.getString(R.string.time_minutes, diff / 60_000L);
} else if (diff < 3600_000L * 24L) {
return context.getString(R.string.time_hours, diff / 3600_000L);
} else {
int days = (int) (diff / (3600_000L * 24L));
if (days > 30) {
ZonedDateTime dt = instant.atZone(ZoneId.systemDefault());
if (dt.getYear() == ZonedDateTime.now().getYear()) {
return DATE_FORMATTER_SHORT.format(dt);
}else{
} else {
return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
}
}
@@ -207,76 +211,77 @@ public class UiUtils {
long diff=now-t;
if(diff<1000L){
return context.getString(R.string.time_just_now);
}else if(diff<60_000L){
int secs=(int)(diff/1000L);
} else if (diff < 60_000L) {
int secs = (int) (diff / 1000L);
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
}else if(diff<3600_000L){
int mins=(int)(diff/60_000L);
} else if (diff < 3600_000L) {
int mins = (int) (diff / 60_000L);
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
}else{
} else {
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
}
}
public static String formatTimeLeft(Context context, Instant instant){
long t=instant.toEpochMilli();
long now=System.currentTimeMillis();
long diff=t-now;
if(diff<60_000L){
int secs=(int)(diff/1000L);
public static String formatTimeLeft(Context context, Instant instant) {
long t = instant.toEpochMilli();
long now = System.currentTimeMillis();
long diff = t - now;
if (diff < 60_000L) {
int secs = (int) (diff / 1000L);
return context.getResources().getQuantityString(R.plurals.x_seconds_left, secs, secs);
}else if(diff<3600_000L){
int mins=(int)(diff/60_000L);
} else if (diff < 3600_000L) {
int mins = (int) (diff / 60_000L);
return context.getResources().getQuantityString(R.plurals.x_minutes_left, mins, mins);
}else if(diff<3600_000L*24L){
int hours=(int)(diff/3600_000L);
} else if (diff < 3600_000L * 24L) {
int hours = (int) (diff / 3600_000L);
return context.getResources().getQuantityString(R.plurals.x_hours_left, hours, hours);
}else{
int days=(int)(diff/(3600_000L*24L));
} else {
int days = (int) (diff / (3600_000L * 24L));
return context.getResources().getQuantityString(R.plurals.x_days_left, days, days);
}
}
@SuppressLint("DefaultLocale")
public static String abbreviateNumber(int n){
if(n<1000){
public static String abbreviateNumber(int n) {
if (n < 1000) {
return String.format("%,d", n);
}else if(n<1_000_000){
float a=n/1000f;
return a>99f ? String.format("%,dK", (int)Math.floor(a)) : String.format("%,.1fK", a);
}else{
float a=n/1_000_000f;
return a>99f ? String.format("%,dM", (int)Math.floor(a)) : String.format("%,.1fM", n/1_000_000f);
} else if (n < 1_000_000) {
float a = n / 1000f;
return a > 99f ? String.format("%,dK", (int) Math.floor(a)) : String.format("%,.1fK", a);
} else {
float a = n / 1_000_000f;
return a > 99f ? String.format("%,dM", (int) Math.floor(a)) : String.format("%,.1fM", n / 1_000_000f);
}
}
@SuppressLint("DefaultLocale")
public static String abbreviateNumber(long n){
if(n<1_000_000_000L)
return abbreviateNumber((int)n);
public static String abbreviateNumber(long n) {
if (n < 1_000_000_000L)
return abbreviateNumber((int) n);
double a=n/1_000_000_000.0;
return a>99f ? String.format("%,dB", (int)Math.floor(a)) : String.format("%,.1fB", n/1_000_000_000.0);
double a = n / 1_000_000_000.0;
return a > 99f ? String.format("%,dB", (int) Math.floor(a)) : String.format("%,.1fB", n / 1_000_000_000.0);
}
/**
* Android 6.0 has a bug where start and end compound drawables don't get tinted.
* This works around it by setting the tint colors directly to the drawables.
*
* @param textView
*/
public static void fixCompoundDrawableTintOnAndroid6(TextView textView){
Drawable[] drawables=textView.getCompoundDrawablesRelative();
for(int i=0;i<drawables.length;i++){
if(drawables[i]!=null){
Drawable tinted=drawables[i].mutate();
public static void fixCompoundDrawableTintOnAndroid6(TextView textView) {
Drawable[] drawables = textView.getCompoundDrawablesRelative();
for (int i = 0; i < drawables.length; i++) {
if (drawables[i] != null) {
Drawable tinted = drawables[i].mutate();
tinted.setTintList(textView.getTextColors());
drawables[i]=tinted;
drawables[i] = tinted;
}
}
textView.setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]);
}
public static void runOnUiThread(Runnable runnable){
public static void runOnUiThread(Runnable runnable) {
mainHandler.post(runnable);
}
@@ -284,67 +289,71 @@ public class UiUtils {
mainHandler.postDelayed(runnable, delay);
}
public static void removeCallbacks(Runnable runnable){
public static void removeCallbacks(Runnable runnable) {
mainHandler.removeCallbacks(runnable);
}
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
/**
* Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}.
*/
public static int lerp(int startValue, int endValue, float fraction) {
return startValue + Math.round(fraction * (endValue - startValue));
}
public static String getFileName(Uri uri){
if(uri.getScheme().equals("content")){
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)){
public static String getFileName(Uri uri) {
if (uri.getScheme().equals("content")) {
try (Cursor cursor = MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
cursor.moveToFirst();
String name=cursor.getString(0);
if(name!=null)
String name = cursor.getString(0);
if (name != null)
return name;
}catch(Throwable ignore){}
} catch (Throwable ignore) {
}
}
return uri.getLastPathSegment();
}
public static String formatFileSize(Context context, long size, boolean atLeastKB){
if(size<1024 && !atLeastKB){
public static String formatFileSize(Context context, long size, boolean atLeastKB) {
if (size < 1024 && !atLeastKB) {
return context.getString(R.string.file_size_bytes, size);
}else if(size<1024*1024){
return context.getString(R.string.file_size_kb, size/1024.0);
}else if(size<1024*1024*1024){
return context.getString(R.string.file_size_mb, size/(1024.0*1024.0));
}else{
return context.getString(R.string.file_size_gb, size/(1024.0*1024.0*1024.0));
} else if (size < 1024 * 1024) {
return context.getString(R.string.file_size_kb, size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return context.getString(R.string.file_size_mb, size / (1024.0 * 1024.0));
} else {
return context.getString(R.string.file_size_gb, size / (1024.0 * 1024.0 * 1024.0));
}
}
public static MediaType getFileMediaType(File file){
String name=file.getName();
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.')+1)));
public static MediaType getFileMediaType(File file) {
String name = file.getName();
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.') + 1)));
}
public static void loadCustomEmojiInTextView(TextView view){
CharSequence _text=view.getText();
if(!(_text instanceof Spanned text))
public static void loadCustomEmojiInTextView(TextView view) {
CharSequence _text = view.getText();
if (!(_text instanceof Spanned))
return;
CustomEmojiSpan[] spans=text.getSpans(0, text.length(), CustomEmojiSpan.class);
if(spans.length==0)
Spanned text = (Spanned) _text;
CustomEmojiSpan[] spans = text.getSpans(0, text.length(), CustomEmojiSpan.class);
if (spans.length == 0)
return;
int emojiSize=V.dp(20);
Map<Emoji, List<CustomEmojiSpan>> spansByEmoji=Arrays.stream(spans).collect(Collectors.groupingBy(s->s.emoji));
for(Map.Entry<Emoji, List<CustomEmojiSpan>> emoji:spansByEmoji.entrySet()){
ViewImageLoader.load(new ViewImageLoader.Target(){
int emojiSize = V.dp(20);
Map<Emoji, List<CustomEmojiSpan>> spansByEmoji = Arrays.stream(spans).collect(Collectors.groupingBy(s -> s.emoji));
for (Map.Entry<Emoji, List<CustomEmojiSpan>> emoji : spansByEmoji.entrySet()) {
ViewImageLoader.load(new ViewImageLoader.Target() {
@Override
public void setImageDrawable(Drawable d){
if(d==null)
public void setImageDrawable(Drawable d) {
if (d == null)
return;
for(CustomEmojiSpan span:emoji.getValue()){
for (CustomEmojiSpan span : emoji.getValue()) {
span.setDrawable(d);
}
view.invalidate();
}
@Override
public View getView(){
public View getView() {
return view;
}
}, null, new UrlImageLoaderRequest(emoji.getKey().url, emojiSize, emojiSize), null, false, true);
@@ -371,22 +380,22 @@ public class UiUtils {
Bundle args = new Bundle();
args.putString("account", selfID);
args.putString("profileAccountID", id);
Nav.go((Activity)context, ProfileFragment.class, args);
Nav.go((Activity) context, ProfileFragment.class, args);
}
public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following){
Bundle args=new Bundle();
public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following) {
Bundle args = new Bundle();
args.putString("account", accountID);
args.putString("hashtag", hashtag);
if (following != null) args.putBoolean("following", following);
Nav.go((Activity)context, HashtagTimelineFragment.class, args);
Nav.go((Activity) context, HashtagTimelineFragment.class, args);
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed) {
showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed);
}
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, 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);
}
@@ -404,25 +413,25 @@ public class UiUtils {
.show();
}
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback){
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),
R.drawable.ic_fluent_person_prohibited_28_regular,
()->{
() -> {
new SetAccountBlocked(account.id, !currentlyBlocked)
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship result){
public void onSuccess(Relationship result) {
if (activity == null) return;
resultCallback.accept(result);
if(!currentlyBlocked){
if (!currentlyBlocked) {
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
}
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
@@ -431,54 +440,54 @@ public class UiUtils {
});
}
public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback){
public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity,
activity.getString(R.string.sk_remove_follower),
activity.getString(R.string.sk_remove_follower_confirm, account.displayName),
activity.getString(R.string.sk_do_remove_follower),
R.drawable.ic_fluent_person_delete_24_regular,
() -> new SetAccountBlocked(account.id, true).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship relationship) {
new SetAccountBlocked(account.id, false).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship relationship) {
if (activity == null) return;
Toast.makeText(activity, R.string.sk_remove_follower_success, Toast.LENGTH_SHORT).show();
resultCallback.accept(relationship);
}
@Override
public void onSuccess(Relationship relationship) {
new SetAccountBlocked(account.id, false).setCallback(new Callback<>() {
@Override
public void onSuccess(Relationship relationship) {
if (activity == null) return;
Toast.makeText(activity, R.string.sk_remove_follower_success, Toast.LENGTH_SHORT).show();
resultCallback.accept(relationship);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
resultCallback.accept(relationship);
}
}).exec(accountID);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
resultCallback.accept(relationship);
}
}).exec(accountID);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
}).exec(accountID)
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
}).exec(accountID)
);
}
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){
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),
R.drawable.ic_fluent_shield_28_regular,
()->{
() -> {
new SetDomainBlocked(domain, !currentlyBlocked)
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Object result){
public void onSuccess(Object result) {
resultCallback.run();
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
@@ -585,9 +594,9 @@ public class UiUtils {
R.string.delete,
R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus.Scheduled(status.id)
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Object o){
public void onSuccess(Object o) {
resultCallback.run();
E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
}
@@ -675,38 +684,39 @@ public class UiUtils {
setRelationshipToActionButton(relationship, button, false);
}
public static void setRelationshipToActionButton(Relationship relationship, Button button, boolean keepText){
public static void setRelationshipToActionButton(Relationship relationship, Button button, boolean keepText) {
CharSequence textBefore = keepText ? button.getText() : null;
boolean secondaryStyle;
if(relationship.blocking){
if (relationship.blocking) {
button.setText(R.string.button_blocked);
secondaryStyle=true;
}else if(relationship.blockedBy){
button.setText(R.string.button_follow);
secondaryStyle=false;
}else if(relationship.requested){
secondaryStyle = true;
// } else if (relationship.blockedBy) {
// button.setText(R.string.button_follow);
// secondaryStyle = false;
} else if (relationship.requested) {
button.setText(R.string.button_follow_pending);
secondaryStyle=true;
}else if(!relationship.following){
secondaryStyle = true;
} else if (!relationship.following) {
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
secondaryStyle=false;
}else{
secondaryStyle = false;
} else {
button.setText(R.string.button_following);
secondaryStyle=true;
secondaryStyle = true;
}
if (keepText) button.setText(textBefore);
button.setEnabled(!relationship.blockedBy);
int attr=secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
TypedArray ta=button.getContext().obtainStyledAttributes(new int[]{attr});
int styleRes=ta.getResourceId(0, 0);
// https://github.com/sk22/megalodon/issues/526
// button.setEnabled(!relationship.blockedBy);
int attr = secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
TypedArray ta = button.getContext().obtainStyledAttributes(new int[]{attr});
int styleRes = ta.getResourceId(0, 0);
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
if(relationship.blocking)
ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
if (relationship.blocking)
button.setTextColor(button.getResources().getColorStateList(R.color.error_600));
else
button.setTextColor(ta.getColorStateList(0));
@@ -721,7 +731,7 @@ public class UiUtils {
public void onSuccess(Relationship result) {
resultCallback.accept(result);
progressCallback.accept(false);
Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username), Toast.LENGTH_SHORT).show();
Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@' + account.username), Toast.LENGTH_SHORT).show();
}
@Override
@@ -839,7 +849,8 @@ public class UiUtils {
@Override
public void onSuccess(Relationship rel) {
E.post(new FollowRequestHandledEvent(accountID, false, account, rel));
if (notificationID != null) E.post(new NotificationDeletedEvent(notificationID));
if (notificationID != null)
E.post(new NotificationDeletedEvent(notificationID));
resultCallback.accept(rel);
}
@@ -852,34 +863,34 @@ public class UiUtils {
}
}
public static <T> void updateList(List<T> oldList, List<T> newList, RecyclerView list, RecyclerView.Adapter<?> adapter, BiPredicate<T, T> areItemsSame){
public static <T> void updateList(List<T> oldList, List<T> newList, RecyclerView list, RecyclerView.Adapter<?> adapter, BiPredicate<T, T> areItemsSame) {
// Save topmost item position and offset because for some reason RecyclerView would scroll the list to weird places when you insert items at the top
int topItem, topItemOffset;
if(list.getChildCount()==0){
topItem=topItemOffset=0;
}else{
View child=list.getChildAt(0);
topItem=list.getChildAdapterPosition(child);
topItemOffset=child.getTop();
if (list.getChildCount() == 0) {
topItem = topItemOffset = 0;
} else {
View child = list.getChildAt(0);
topItem = list.getChildAdapterPosition(child);
topItemOffset = child.getTop();
}
DiffUtil.calculateDiff(new DiffUtil.Callback(){
DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize(){
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize(){
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsSame.test(oldList.get(oldItemPosition), newList.get(newItemPosition));
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return true;
}
}).dispatchUpdatesTo(adapter);
@@ -887,34 +898,35 @@ public class UiUtils {
list.scrollBy(0, topItemOffset);
}
public static Bitmap getBitmapFromDrawable(Drawable d){
if(d instanceof BitmapDrawable)
public static Bitmap getBitmapFromDrawable(Drawable d) {
if (d instanceof BitmapDrawable)
return ((BitmapDrawable) d).getBitmap();
Bitmap bitmap=Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
d.draw(new Canvas(bitmap));
return bitmap;
}
public static void insetPopupMenuIcon(Context context, MenuItem item) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
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);
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);
icon = new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
SpannableStringBuilder ssb = new SpannableStringBuilder(item.getTitle());
item.setTitle(ssb);
}
public static void resetPopupItemTint(MenuItem item) {
if(Build.VERSION.SDK_INT>=26) {
if (Build.VERSION.SDK_INT >= 26) {
item.setIconTintList(null);
} else {
Drawable icon=item.getIcon().mutate();
Drawable icon = item.getIcon().mutate();
icon.setTintList(null);
item.setIcon(icon);
}
@@ -923,7 +935,7 @@ public class UiUtils {
/// Add icons to the menu.
/// Passing in items will be colored to be visible on the background.
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
if(menu.getClass().getSimpleName().equals("MenuBuilder")){
if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
try {
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
@@ -935,31 +947,33 @@ public class UiUtils {
}
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);
ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for (int i = 0; i < m.size(); i++) {
MenuItem item = m.getItem(i);
SubMenu subMenu = item.getSubMenu();
if (subMenu != null) enableMenuIcons(context, subMenu, exclude);
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue;
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){
public static void enablePopupMenuIcons(Context context, PopupMenu menu) {
Menu m = menu.getMenu();
if (Build.VERSION.SDK_INT >= 29) {
menu.setForceShowIcon(true);
}else{
try{
Method setOptionalIconsVisible=m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
} else {
try {
Method setOptionalIconsVisible = m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
setOptionalIconsVisible.setAccessible(true);
setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){}
} catch (Exception ignore) {
}
}
enableMenuIcons(context, m);
}
public static void setUserPreferredTheme(Context context){
public static void setUserPreferredTheme(Context context) {
context.setTheme(switch (theme) {
case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
@@ -969,10 +983,11 @@ public class UiUtils {
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
if (palette != null) palette.apply(context);
}
public static boolean isDarkTheme(){
if(theme==GlobalUserPreferences.ThemePreference.AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
return theme==GlobalUserPreferences.ThemePreference.DARK;
public static boolean isDarkTheme() {
if (theme == GlobalUserPreferences.ThemePreference.AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
return theme == GlobalUserPreferences.ThemePreference.DARK;
}
// https://mastodon.foo.bar/@User
@@ -1001,7 +1016,8 @@ public class UiUtils {
return false;
}
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null) return false;
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null)
return false;
String it = uri.getPath();
return it.matches("^/@[^/]+$") ||
@@ -1021,13 +1037,13 @@ public class UiUtils {
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;
Optional<Instance> instance = session.getInstance();
return instance.isPresent() && !instance.get().title.isBlank() ? instance.get().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());
List<AccountSession> sessions = AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s -> !s.getID().equals(exceptFor)).collect(Collectors.toList());
AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
.setItems(
@@ -1140,7 +1156,7 @@ public class UiUtils {
error.showToast(context);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID);
}
@@ -1250,28 +1266,36 @@ public class UiUtils {
}
}
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())){
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){
public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
lookupURL(context, accountID, url, launchBrowser, (clazz, args) -> {
if (clazz == null) return;
Nav.go((Activity) context, clazz, args);
});
}
public static void lookupURL(Context context, String accountID, String url, boolean launchBrowser, BiConsumer<Class<? extends Fragment>, Bundle> go) {
Uri uri = Uri.parse(url);
List<String> path = uri.getPathSegments();
if (accountID != null && "https".equals(uri.getScheme())) {
if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
new GetStatusByID(path.get(1))
.setCallback(new Callback<>(){
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result){
Bundle args=new Bundle();
public void onSuccess(Status result) {
Bundle args = new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result));
Nav.go((Activity) context, ThreadFragment.class, args);
go.accept(ThreadFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
@@ -1280,54 +1304,62 @@ public class UiUtils {
.setCallback(new Callback<>() {
@Override
public void onSuccess(SearchResults results) {
Bundle args=new Bundle();
Bundle args = new Bundle();
args.putString("account", accountID);
if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
Nav.go((Activity) context, ThreadFragment.class, args);
} else if (!results.accounts.isEmpty()) {
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
Nav.go((Activity) context, ProfileFragment.class, args);
} else {
if (launchBrowser) launchWebBrowser(context, url);
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(ThreadFragment.class, args);
return;
}
Optional<Account> account = results.accounts.stream()
.filter(a -> uri.equals(Uri.parse(a.url))).findAny();
if (account.isPresent()) {
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
go.accept(ProfileFragment.class, args);
return;
}
if (launchBrowser) launchWebBrowser(context, url);
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
go.accept(null, null);
}
@Override
public void onError(ErrorResponse error) {
error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
}
})
.wrapProgress((Activity)context, R.string.loading, true,
.wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID);
return;
}
}
launchWebBrowser(context, url);
if (launchBrowser) launchWebBrowser(context, url);
go.accept(null, null);
}
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
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();
}
v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
private static String getSystemProperty(String key){
try{
Class<?> props=Class.forName("android.os.SystemProperties");
Method get=props.getMethod("get", String.class);
return (String)get.invoke(null, key);
}catch(Exception ignore){}
private static String getSystemProperty(String key) {
try {
Class<?> props = Class.forName("android.os.SystemProperties");
Method get = props.getMethod("get", String.class);
return (String) get.invoke(null, key);
} catch (Exception ignore) {
}
return null;
}
public static boolean isMIUI(){
public static boolean isMIUI() {
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
}
@@ -1335,17 +1367,25 @@ public class UiUtils {
return !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui"));
}
public static int alphaBlendColors(int color1, int color2, float alpha) {
float alpha0 = 1f - alpha;
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);
int g = Math.round(((color1 >> 8) & 0xFF) * alpha0 + ((color2 >> 8) & 0xFF) * alpha);
int b = Math.round((color1 & 0xFF) * alpha0 + (color2 & 0xFF) * alpha);
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
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){
public static boolean pickAccountForCompose(Activity activity, String accountID) {
return pickAccountForCompose(activity, accountID, (String) null);
}
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args){
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());

View File

@@ -0,0 +1,62 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.RelativeLayout;
public class CheckableRelativeLayout extends RelativeLayout implements Checkable{
private boolean checked, checkable = true;
private static final int[] CHECKED_STATE_SET = {
android.R.attr.state_checked
};
public CheckableRelativeLayout(Context context){
this(context, null);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
public void setChecked(boolean checked){
this.checked=checked;
refreshDrawableState();
}
public void setCheckable(boolean checkable) {
this.checkable = checkable;
}
@Override
public boolean isChecked(){
return checked;
}
@Override
public void toggle(){
setChecked(!checked);
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
super.onInitializeAccessibilityNodeInfo(info);
info.setCheckable(checkable);
info.setChecked(checked);
}
}

View File

@@ -0,0 +1,32 @@
package org.joinmastodon.android.utils;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.net.Uri;
import org.joinmastodon.android.fragments.HasAccountID;
public interface ProvidesAssistContent {
void onProvideAssistContent(AssistContent assistContent);
default boolean callFragmentToProvideAssistContent(Fragment fragment, AssistContent assistContent) {
if (fragment instanceof ProvidesAssistContent assistiveFragment) {
assistiveFragment.onProvideAssistContent(assistContent);
return true;
} else {
return false;
}
}
interface ProvidesWebUri extends ProvidesAssistContent, HasAccountID {
Uri getWebUri(Uri.Builder base);
default Uri.Builder getUriBuilder() {
return getSession().getInstanceUri().buildUpon();
}
default void onProvideAssistContent(AssistContent assistContent) {
assistContent.setWebUri(getWebUri(getUriBuilder()));
}
}
}