Commit Diff


commit - a4f512fa7a99d7d5e510ea2cec0efa0d721d42a1
commit + 3f6b305219492b01af706e58faf00679164ebb6b
blob - 3aebcd1ad8674971d61d33d43f0dc8792596edc6 (mode 644)
blob + /dev/null
--- mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java
+++ /dev/null
@@ -1,377 +0,0 @@
-package org.joinmastodon.android.api.session;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import org.joinmastodon.android.E;
-import org.joinmastodon.android.MastodonApp;
-import org.joinmastodon.android.R;
-import org.joinmastodon.android.api.CacheController;
-import org.joinmastodon.android.api.MastodonAPIController;
-import org.joinmastodon.android.api.PushSubscriptionManager;
-import org.joinmastodon.android.api.StatusInteractionController;
-import org.joinmastodon.android.api.requests.accounts.GetPreferences;
-import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
-import org.joinmastodon.android.api.requests.markers.GetMarkers;
-import org.joinmastodon.android.api.requests.markers.SaveMarkers;
-import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
-import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
-import org.joinmastodon.android.model.Account;
-import org.joinmastodon.android.model.AltTextFilter;
-import org.joinmastodon.android.model.Application;
-import org.joinmastodon.android.model.FilterAction;
-import org.joinmastodon.android.model.FilterContext;
-import org.joinmastodon.android.model.FilterResult;
-import org.joinmastodon.android.model.Instance;
-import org.joinmastodon.android.model.FollowList;
-import org.joinmastodon.android.model.LegacyFilter;
-import org.joinmastodon.android.model.Preferences;
-import org.joinmastodon.android.model.PushSubscription;
-import org.joinmastodon.android.model.Status;
-import org.joinmastodon.android.model.TimelineMarkers;
-import org.joinmastodon.android.model.Token;
-import org.joinmastodon.android.utils.ObjectIdComparator;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.Consumer;
-import java.util.function.Function;
-
-import me.grishka.appkit.api.Callback;
-import me.grishka.appkit.api.ErrorResponse;
-
-public class AccountSession{
-	private static final String TAG="AccountSession";
-
-	public Token token;
-	public Account self;
-	public String domain;
-	public Application app;
-	public long infoLastUpdated;
-	public boolean activated=true;
-	public String pushPrivateKey;
-	public String pushPublicKey;
-	public String pushAuthKey;
-	public PushSubscription pushSubscription;
-	public boolean needUpdatePushSettings;
-	public long filtersLastUpdated;
-	public List<LegacyFilter> wordFilters=new ArrayList<>();
-	public String pushAccountID;
-	public AccountActivationInfo activationInfo;
-	public Preferences preferences;
-	private transient MastodonAPIController apiController;
-	private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController;
-	private transient CacheController cacheController;
-	private transient PushSubscriptionManager pushSubscriptionManager;
-	private transient SharedPreferences prefs;
-	private transient boolean preferencesNeedSaving;
-	private transient AccountLocalPreferences localPreferences;
-	private transient List<FollowList> lists;
-
-	AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
-		this.token=token;
-		this.self=self;
-		this.domain=domain;
-		this.app=app;
-		this.activated=activated;
-		this.activationInfo=activationInfo;
-		infoLastUpdated=System.currentTimeMillis();
-	}
-
-	AccountSession(){}
-
-	public String getID(){
-		return domain+"_"+self.id;
-	}
-
-	public MastodonAPIController getApiController(){
-		if(apiController==null)
-			apiController=new MastodonAPIController(this);
-		return apiController;
-	}
-
-	public StatusInteractionController getStatusInteractionController(){
-		if(statusInteractionController==null)
-			statusInteractionController=new StatusInteractionController(getID());
-		return statusInteractionController;
-	}
-
-	public StatusInteractionController getRemoteStatusInteractionController(){
-		if(remoteStatusInteractionController==null)
-			remoteStatusInteractionController=new StatusInteractionController(getID(), false);
-		return remoteStatusInteractionController;
-	}
-
-	public CacheController getCacheController(){
-		if(cacheController==null)
-			cacheController=new CacheController(getID());
-		return cacheController;
-	}
-
-	public PushSubscriptionManager getPushSubscriptionManager(){
-		if(pushSubscriptionManager==null)
-			pushSubscriptionManager=new PushSubscriptionManager(getID());
-		return pushSubscriptionManager;
-	}
-
-	public String getFullUsername(){
-		return '@'+self.username+'@'+domain;
-	}
-
-	public void preferencesFromAccountSource(Account account) {
-		if (account != null && account.source != null && preferences != null) {
-			if (account.source.privacy != null)
-				preferences.postingDefaultVisibility = account.source.privacy;
-			if (account.source.language != null)
-				preferences.postingDefaultLanguage = account.source.language;
-		}
-	}
-
-	public void reloadPreferences(Consumer<Preferences> callback){
-		new GetPreferences()
-				.setCallback(new Callback<>(){
-					@Override
-					public void onSuccess(Preferences result){
-						preferences=result;
-						preferencesFromAccountSource(self);
-						if(callback!=null)
-							callback.accept(result);
-						AccountSessionManager.getInstance().writeAccountsFile();
-					}
-
-					@Override
-					public void onError(ErrorResponse error){
-						Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
-						if (preferences==null)
-							preferences=new Preferences();
-						preferencesFromAccountSource(self);
-					}
-				})
-				.exec(getID());
-	}
-
-	public SharedPreferences getRawLocalPreferences(){
-		if(prefs==null)
-			prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
-		return prefs;
-	}
-
-	public void reloadNotificationsMarker(Consumer<String> callback){
-		new GetMarkers()
-				.setCallback(new Callback<>(){
-					@Override
-					public void onSuccess(TimelineMarkers result){
-						if(result.notifications!=null && !TextUtils.isEmpty(result.notifications.lastReadId)){
-							String id=result.notifications.lastReadId;
-							String lastKnown=getLastKnownNotificationsMarker();
-							if(ObjectIdComparator.INSTANCE.compare(id, lastKnown)<0){
-								// Marker moved back -- previous marker update must have failed.
-								// Pretend it didn't happen and repeat the request.
-								id=lastKnown;
-								new SaveMarkers(null, id).exec(getID());
-							}
-							callback.accept(id);
-							setNotificationsMarker(id, false);
-						}
-					}
-
-					@Override
-					public void onError(ErrorResponse error){}
-				})
-				.exec(getID());
-	}
-
-	public String getLastKnownNotificationsMarker(){
-		return getRawLocalPreferences().getString("notificationsMarker", null);
-	}
-
-	public void setNotificationsMarker(String id, boolean clearUnread){
-		getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
-		E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
-	}
-
-	public void logOut(Activity activity, Runnable onDone){
-		new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
-				.setCallback(new Callback<>(){
-					@Override
-					public void onSuccess(Object result){
-						AccountSessionManager.getInstance().removeAccount(getID());
-						onDone.run();
-					}
-
-					@Override
-					public void onError(ErrorResponse error){
-						AccountSessionManager.getInstance().removeAccount(getID());
-						onDone.run();
-					}
-				})
-				.wrapProgress(activity, R.string.loading, false)
-				.exec(getID());
-	}
-
-	public void savePreferencesLater(){
-		preferencesNeedSaving=true;
-	}
-
-	public void savePreferencesIfPending(){
-		if(preferencesNeedSaving){
-			new UpdateAccountCredentialsPreferences(preferences, self.locked, self.discoverable, self.source.indexable)
-					.setCallback(new Callback<>(){
-						@Override
-						public void onSuccess(Account result){
-							preferencesNeedSaving=false;
-							self=result;
-							AccountSessionManager.getInstance().writeAccountsFile();
-						}
-
-						@Override
-						public void onError(ErrorResponse error){
-							Log.e(TAG, "failed to save preferences: "+error);
-						}
-					})
-					.exec(getID());
-		}
-	}
-
-	public AccountLocalPreferences getLocalPreferences(){
-		if(localPreferences==null)
-			localPreferences=new AccountLocalPreferences(getRawLocalPreferences(), this);
-		return localPreferences;
-	}
-
-	public void filterStatuses(List<Status> statuses, FilterContext context){
-		filterStatuses(statuses, context, null);
-	}
-
-	public void filterStatuses(List<Status> statuses, FilterContext context, Account profile){
-		filterStatusContainingObjects(statuses, Function.identity(), context, profile);
-	}
-
-	public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context){
-		filterStatusContainingObjects(objects, extractor, context, null);
-	}
-
-	private boolean statusIsOnOwnProfile(Status s, Account profile){
-		return self != null && profile != null && s.account != null
-				&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
-	}
-
-	private boolean isFilteredType(Status s){
-		AccountLocalPreferences localPreferences = getLocalPreferences();
-		return (!localPreferences.showReplies && s.inReplyToId != null)
-				|| (!localPreferences.showBoosts && s.reblog != null);
-	}
-
-	public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
-		AccountLocalPreferences localPreferences = getLocalPreferences();
-		if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
-			Status s=extractor.apply(obj);
-			if(s!=null && s.filtered!=null){
-				localPreferences.serverSideFiltersSupported=true;
-				localPreferences.save();
-				break;
-			}
-		}
-
-		List<T> removeUs=new ArrayList<>();
-		for(int i=0; i<objects.size(); i++){
-			T o=objects.get(i);
-			if(filterStatusContainingObject(o, extractor, context, profile)){
-				Status s=extractor.apply(o);
-				removeUs.add(o);
-				if(s!=null && s.hasGapAfter!=null && i>0){
-					// oops, we're about to remove an item that has a gap after...
-					// gotta find the previous status that's not also about to be removed
-					for(int j=i-1; j>=0; j--){
-						T p=objects.get(j);
-						Status prev=extractor.apply(objects.get(j));
-						if(prev!=null && !removeUs.contains(p)){
-							prev.hasGapAfter=s.hasGapAfter;
-							break;
-						}
-					}
-				}
-			}
-		}
-		objects.removeAll(removeUs);
-	}
-
-	public <T> boolean filterStatusContainingObject(T object, Function<T, Status> extractor, FilterContext context, Account profile){
-		Status s=extractor.apply(object);
-		if(s==null)
-			return false;
-		// don't hide own posts in own profile
-		if(statusIsOnOwnProfile(s, profile))
-			return false;
-		if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC))
-			return true;
-		// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
-		if(getLocalPreferences().serverSideFiltersSupported){
-			// Moshidon: this code path in CustomLocalTimelines makes the app crash, so this check is here
-			if (s.filtered == null)
-				return false;
-			for(FilterResult filter : s.filtered){
-				if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE && filter.filter.context.contains(context))
-					return true;
-			}
-		}else if(wordFilters!=null){
-			for(LegacyFilter filter : wordFilters){
-				if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
-					return true;
-			}
-		}
-		return false;
-	}
-
-	public List<FilterResult> getClientSideFilters(Status status) {
-		List<FilterResult> filters = new ArrayList<>();
-
-		// filter post that have no alt text
-		// it only applies when activated in the settings
-		AltTextFilter altTextFilter=new AltTextFilter(FilterAction.WARN, EnumSet.allOf(FilterContext.class));
-		if(altTextFilter.matches(status)){
-			FilterResult filterResult=new FilterResult();
-			filterResult.filter=altTextFilter;
-			filterResult.keywordMatches=List.of();
-			filters.add(filterResult);
-		}
-		return filters;
-	}
-
-	public void updateAccountInfo(){
-		AccountSessionManager.getInstance().updateSessionLocalInfo(this);
-	}
-
-	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();
-	}
-
-	public String getDefaultAvatarUrl() {
-		return getInstance()
-				.map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png"))
-				.orElse("");
-	}
-
-	public boolean isNotificationsMentionsOnly(){
-		return getRawLocalPreferences().getBoolean("notificationsMentionsOnly", false);
-	}
-
-	public void setNotificationsMentionsOnly(boolean mentionsOnly){
-		getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
-	}
-}
blob - /dev/null
blob + c55bb9e9b37fa74dc67e69b9df7982f24caf81cf (mode 644)
--- /dev/null
+++ mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.kt
@@ -0,0 +1,418 @@
+package org.joinmastodon.android.api.session
+
+import android.app.Activity
+import android.content.Context
+import android.content.SharedPreferences
+import android.net.Uri
+import android.text.TextUtils
+import android.util.Log
+import me.grishka.appkit.api.Callback
+import me.grishka.appkit.api.ErrorResponse
+import org.joinmastodon.android.E
+import org.joinmastodon.android.MastodonApp
+import org.joinmastodon.android.R
+import org.joinmastodon.android.api.CacheController
+import org.joinmastodon.android.api.MastodonAPIController
+import org.joinmastodon.android.api.PushSubscriptionManager
+import org.joinmastodon.android.api.StatusInteractionController
+import org.joinmastodon.android.api.requests.accounts.GetPreferences
+import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences
+import org.joinmastodon.android.api.requests.markers.GetMarkers
+import org.joinmastodon.android.api.requests.markers.SaveMarkers
+import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken
+import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent
+import org.joinmastodon.android.model.Account
+import org.joinmastodon.android.model.AltTextFilter
+import org.joinmastodon.android.model.Application
+import org.joinmastodon.android.model.FilterAction
+import org.joinmastodon.android.model.FilterContext
+import org.joinmastodon.android.model.FilterResult
+import org.joinmastodon.android.model.Instance
+import org.joinmastodon.android.model.LegacyFilter
+import org.joinmastodon.android.model.Preferences
+import org.joinmastodon.android.model.PushSubscription
+import org.joinmastodon.android.model.Status
+import org.joinmastodon.android.model.TimelineMarkers
+import org.joinmastodon.android.model.Token
+import org.joinmastodon.android.utils.ObjectIdComparator
+import java.util.EnumSet
+import java.util.Optional
+import java.util.function.Consumer
+import java.util.function.Function
+
+class AccountSession {
+    @JvmField
+    var token: Token? = null
+    @JvmField
+    var self: Account? = null
+    @JvmField
+    var domain: String? = null
+    @JvmField
+    var app: Application? = null
+    @JvmField
+    var infoLastUpdated: Long = 0
+    @JvmField
+    var activated: Boolean = true
+    @JvmField
+    var pushPrivateKey: String? = null
+    @JvmField
+    var pushPublicKey: String? = null
+    @JvmField
+    var pushAuthKey: String? = null
+    @JvmField
+    var pushSubscription: PushSubscription? = null
+    @JvmField
+    var needUpdatePushSettings: Boolean = false
+    @JvmField
+    var filtersLastUpdated: Long = 0
+    @JvmField
+    var wordFilters: MutableList<LegacyFilter>? = ArrayList()
+    @JvmField
+    var pushAccountID: String? = null
+    @JvmField
+    var activationInfo: AccountActivationInfo? = null
+    @JvmField
+    var preferences: Preferences? = null
+
+	val lastKnownNotificationsMarker: String?
+		get() = this.rawLocalPreferences?.getString("notificationsMarker", null)
+
+    @Transient
+    var apiController: MastodonAPIController? = null
+        get() {
+            if (field == null) field = MastodonAPIController(this)
+            return field
+        }
+        private set
+
+    @Transient
+    var statusInteractionController: StatusInteractionController? = null
+        get() {
+            if (field == null) field = StatusInteractionController(this.iD)
+            return field
+        }
+        private set
+
+    @Transient
+    var remoteStatusInteractionController: StatusInteractionController? = null
+        get() {
+            if (field == null) field = StatusInteractionController(this.iD, false)
+            return field
+        }
+        private set
+
+    @Transient
+    var cacheController: CacheController? = null
+        get() {
+            if (field == null) field = CacheController(this.iD)
+            return field
+        }
+        private set
+
+    @Transient
+    var pushSubscriptionManager: PushSubscriptionManager? = null
+        get() {
+            if (field == null) field = PushSubscriptionManager(this.iD)
+            return field
+        }
+        private set
+
+    @Transient
+    private var prefs: SharedPreferences? = null
+
+    @Transient
+    private var preferencesNeedSaving = false
+
+    @Transient
+    var localPreferences: AccountLocalPreferences? = null
+        get() {
+            if (field == null) field = AccountLocalPreferences(this.rawLocalPreferences, this)
+            return field
+        }
+        private set
+
+    internal constructor(
+        token: Token,
+        self: Account,
+        app: Application,
+        domain: String?,
+        activated: Boolean,
+        activationInfo: AccountActivationInfo?
+    ) {
+        this.token = token
+        this.self = self
+        this.domain = domain
+        this.app = app
+        this.activated = activated
+        this.activationInfo = activationInfo
+        infoLastUpdated = System.currentTimeMillis()
+    }
+
+    internal constructor()
+
+    val iD: String
+        get() = domain + "_" + self?.id
+
+    val fullUsername: String
+        get() = '@'.toString() + self?.username + '@' + domain
+
+    fun preferencesFromAccountSource(account: Account?) {
+        if (account != null && account.source != null && preferences != null) {
+            if (account.source.privacy != null) preferences!!.postingDefaultVisibility =
+                account.source.privacy
+            if (account.source.language != null) preferences!!.postingDefaultLanguage =
+                account.source.language
+        }
+    }
+
+    fun reloadPreferences(callback: Consumer<Preferences>?) {
+        GetPreferences()
+            .setCallback(object : Callback<Preferences> {
+                override fun onSuccess(result: Preferences) {
+                    preferences = result
+                    preferencesFromAccountSource(self)
+					callback?.accept(result)
+                    AccountSessionManager.getInstance().writeAccountsFile()
+                }
+
+                override fun onError(error: ErrorResponse) {
+                    Log.w(TAG, "Failed to load preferences for account $iD: $error")
+                    if (preferences == null) preferences = Preferences()
+                    preferencesFromAccountSource(self)
+                }
+            })
+            .exec(this.iD)
+    }
+
+    val rawLocalPreferences: SharedPreferences?
+        get() {
+            if (prefs == null) prefs = MastodonApp.context.getSharedPreferences(
+                this.iD,
+                Context.MODE_PRIVATE
+            )
+            return prefs
+        }
+
+    fun reloadNotificationsMarker(callback: Consumer<String?>) {
+        GetMarkers()
+            .setCallback(object : Callback<TimelineMarkers> {
+                override fun onSuccess(result: TimelineMarkers) {
+                    if (result.notifications != null && !TextUtils.isEmpty(result.notifications.lastReadId)) {
+                        var id = result.notifications.lastReadId
+                        val lastKnown: String? = lastKnownNotificationsMarker
+                        if (ObjectIdComparator.INSTANCE.compare(id, lastKnown) < 0) {
+                            // Marker moved back -- previous marker update must have failed.
+                            // Pretend it didn't happen and repeat the request.
+                            id = lastKnown
+                            SaveMarkers(null, id).exec(iD)
+                        }
+                        callback.accept(id)
+                        setNotificationsMarker(id, false)
+                    }
+                }
+
+                override fun onError(error: ErrorResponse?) {}
+            })
+            .exec(this.iD)
+    }
+
+    fun setNotificationsMarker(id: String?, clearUnread: Boolean) {
+		this.rawLocalPreferences?.edit()?.putString("notificationsMarker", id)?.apply()
+        E.post(NotificationsMarkerUpdatedEvent(this.iD, id, clearUnread))
+    }
+
+    fun logOut(activity: Activity?, onDone: Runnable) {
+        RevokeOauthToken(app?.clientId, app?.clientSecret, token?.accessToken)
+            .setCallback(object : Callback<Any?> {
+                override fun onSuccess(result: Any?) {
+                    AccountSessionManager.getInstance().removeAccount(iD)
+                    onDone.run()
+                }
+
+                override fun onError(error: ErrorResponse?) {
+                    AccountSessionManager.getInstance().removeAccount(iD)
+                    onDone.run()
+                }
+            })
+            .wrapProgress(activity, R.string.loading, false)
+            .exec(this.iD)
+    }
+
+    fun savePreferencesLater() {
+        preferencesNeedSaving = true
+    }
+
+    fun savePreferencesIfPending() {
+        if (preferencesNeedSaving) {
+            UpdateAccountCredentialsPreferences(
+                preferences,
+                self?.locked,
+                self?.discoverable,
+                self?.source?.indexable
+            )
+                .setCallback(object : Callback<Account?> {
+                    override fun onSuccess(result: Account?) {
+                        preferencesNeedSaving = false
+                        self = result
+                        AccountSessionManager.getInstance().writeAccountsFile()
+                    }
+
+                    override fun onError(error: ErrorResponse?) {
+                        Log.e(TAG, "failed to save preferences: $error")
+                    }
+                })
+                .exec(this.iD)
+        }
+    }
+
+    @JvmOverloads
+    fun filterStatuses(
+        statuses: MutableList<Status?>,
+        context: FilterContext?,
+        profile: Account? = null
+    ) {
+        filterStatusContainingObjects<Status?>(
+            statuses,
+            Function.identity<Status?>(),
+            context,
+            profile
+        )
+    }
+
+    fun <T> filterStatusContainingObjects(
+        objects: MutableList<T?>,
+        extractor: Function<T?, Status?>,
+        context: FilterContext?
+    ) {
+        filterStatusContainingObjects<T?>(objects, extractor, context, null)
+    }
+
+    private fun statusIsOnOwnProfile(s: Status, profile: Account?): Boolean {
+        return self != null && profile != null && s.account != null && self!!.id == profile.id && self!!.id == s.account.id
+    }
+
+    private fun isFilteredType(s: Status): Boolean {
+        val localPreferences = this.localPreferences!!
+        return (!localPreferences.showReplies && s.inReplyToId != null)
+                || (!localPreferences.showBoosts && s.reblog != null)
+    }
+
+    fun <T> filterStatusContainingObjects(
+        objects: MutableList<T?>,
+        extractor: Function<T?, Status?>,
+        context: FilterContext?,
+        profile: Account?
+    ) {
+        val localPreferences = this.localPreferences!!
+        if (!localPreferences.serverSideFiltersSupported) for (obj in objects) {
+            val s = extractor.apply(obj)
+            if (s != null && s.filtered != null) {
+                localPreferences.serverSideFiltersSupported = true
+                localPreferences.save()
+                break
+            }
+        }
+
+        val removeUs: MutableList<T?> = ArrayList()
+        for (i in objects.indices) {
+            val o = objects[i]
+            if (filterStatusContainingObject<T?>(o, extractor, context, profile)) {
+                val s = extractor.apply(o)
+                removeUs.add(o)
+                if (s != null && s.hasGapAfter != null && i > 0) {
+                    // oops, we're about to remove an item that has a gap after...
+                    // gotta find the previous status that's not also about to be removed
+                    for (j in i - 1 downTo 0) {
+                        val p = objects[j]
+                        val prev = extractor.apply(objects[j])
+                        if (prev != null && !removeUs.contains(p)) {
+                            prev.hasGapAfter = s.hasGapAfter
+                            break
+                        }
+                    }
+                }
+            }
+        }
+        objects.removeAll(removeUs)
+    }
+
+    fun <T> filterStatusContainingObject(
+        `object`: T?,
+        extractor: Function<T?, Status?>,
+        context: FilterContext?,
+        profile: Account?
+    ): Boolean {
+		val s = extractor.apply(`object`) ?: return false
+		// don't hide own posts in own profile
+        if (statusIsOnOwnProfile(s, profile)) return false
+        if (isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC)) return true
+        // Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
+        if (this.localPreferences!!.serverSideFiltersSupported) {
+            // Moshidon: this code path in CustomLocalTimelines makes the app crash, so this check is here
+            if (s.filtered == null) return false
+            for (filter in s.filtered) {
+                if (filter.filter.isActive && filter.filter.filterAction == FilterAction.HIDE && filter.filter.context.contains(
+                        context
+                    )
+                ) return true
+            }
+        } else if (wordFilters != null) {
+            for (filter in wordFilters) {
+                if (filter.context.contains(context) && filter.matches(s) && filter.isActive) return true
+            }
+        }
+        return false
+    }
+
+    fun getClientSideFilters(status: Status): MutableList<FilterResult?> {
+        val filters: MutableList<FilterResult?> = ArrayList()
+
+        // filter post that have no alt text
+        // it only applies when activated in the settings
+        val altTextFilter = AltTextFilter(
+            FilterAction.WARN,
+            EnumSet.allOf(FilterContext::class.java)
+        )
+        if (altTextFilter.matches(status)) {
+            val filterResult = FilterResult()
+            filterResult.filter = altTextFilter
+            filterResult.keywordMatches = mutableListOf<String?>()
+            filters.add(filterResult)
+        }
+        return filters
+    }
+
+    fun updateAccountInfo() {
+        AccountSessionManager.getInstance().updateSessionLocalInfo(this)
+    }
+
+    val instance: Optional<Instance>
+        get() = Optional.ofNullable<Instance>(
+            AccountSessionManager.getInstance().getInstanceInfo(domain)
+        )
+
+    val instanceUri: Uri?
+        get() = Uri.Builder()
+            .scheme("https")
+            .authority(
+                this.instance.map(Function { i: Instance -> i.normalizedUri })
+                    .orElse(domain)
+            )
+            .build()
+
+    val defaultAvatarUrl: String
+        get() = this.instance
+            .map(Function { instance: Instance -> "https://" + domain + (if (instance.isAkkoma) "/images/avi.png" else "/avatars/original/missing.png") })
+            .orElse("")
+
+    var isNotificationsMentionsOnly: Boolean
+        get() = this.rawLocalPreferences!!.getBoolean("notificationsMentionsOnly", false)
+        set(mentionsOnly) {
+            this.rawLocalPreferences!!.edit().putBoolean("notificationsMentionsOnly", mentionsOnly)
+                .apply()
+        }
+
+    companion object {
+        private const val TAG = "AccountSession"
+    }
+}