Merge upstream redesign (#714)

* merge toolbar fragment

* Fix store screenshot generator

* Fix alert color

* Fix #609

* Fix crash

* bigger hitbox for chips

* support mastodon languages

* merge ui utils

* merge stuff

* fix icon

* ensure 48dp touch target

* init local prefs, add helper function for enum values

* update compose action layout

* merge compose-adj files

* update extended footer

* fix poll wrong option checked

closes sk22#641

* no border when disabled

closes sk22#640

* Fix #610

* Minor fixes

* Fix alert color

* Fix #609

* Fix crash

* Fix #610

* Minor fixes

* add resources

* more compatible mastodon language

* fix html parser

* mark as read on refresh

* update tab bar

* tweak m3 buttons

* update compose-adj files

* tweak and update styles

* m3 expand button

* flag icon should be 18dp, actually

* More minor fixes

closes #612

* More minor fixes

closes #612

* Bump version

* fix no create status event when redrafting

* add material 3 assets

* New translations strings.xml (Greek)

* New translations strings.xml (Greek)

* New translations strings.xml (Italian)

* New translations strings.xml (Greek)

* New translations strings.xml (Italian)

* New translations strings.xml (Thai)

* New translations strings.xml (Thai)

* New translations strings.xml (Italian)

* New translations strings.xml (Thai)

* use new buttons for profile fragment

* merge compose fragment

* merge all the styles! oh dear

* New translations full_description.txt (Indonesian)

* New translations full_description.txt (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)

* New translations full_description.txt (Chinese Simplified)

* Fix #615

* Minor fixes

* Fix #611

* A bunch of crash fixes

* New translations strings.xml (Greek)

* Make the default server configurable

* Pass the system timezone to server when signing up

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Japanese)

* Fix #615

* Minor fixes

* Fix #611

* A bunch of crash fixes

* Make the default server configurable

* Pass the system timezone to server when signing up

* oops. accidentally pasted the commit message in the code

* Remove unused code that caused a crash for some users ¯\_(ツ)_/¯

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* Remove unused code that caused a crash for some users ¯\_(ツ)_/¯

* New translations strings.xml (Polish)

* New translations strings.xml (Polish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Belarusian)

* prepare merging profile fragment

* merge profile fragment

* New translations strings.xml (Belarusian)

* New translations strings.xml (Greek)

* fix icon padding

* apply post header changes

* minor margin tweaks

* fix footer buttons

* fix header announcement buttons

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations full_description.txt (Japanese)

* New translations strings.xml (Icelandic)

* New translations strings.xml (Icelandic)

* New translations strings.xml (Icelandic)

* fix replying

* New translations strings.xml (Icelandic)

* fix translate button

* fix more button visibility

* fix counts label styling

* fix disabled boost button opacity

* fix tab layouts

* fix notification icon color crash

* New translations strings.xml (Greek)

* implement elevation listener in home tab

* fix elevation and listener in home tab

* add elevation scroll listener to notifications

* New translations strings.xml (Scottish Gaelic)

* Add editorconfig

So that PRs like #625 don't happen again

* Crash fix

* 🤔

* New translations strings.xml (Greek)

* New translations strings.xml (Japanese)

* New translations strings.xml (French)

* New translations strings.xml (French)

* New translations strings.xml (French)

* fix notification elevation and integrate divider

* 🤔

* Crash fix

* Add editorconfig

So that PRs like #625 don't happen again

* New translations strings.xml (Turkish)

* save interactions in cache

* New translations strings.xml (Turkish)

* merge new discover/search

* New translations strings.xml (Bengali)

* New translations strings.xml (Scottish Gaelic)

* New translations strings.xml (Bengali)

* merge new settings fragments

* fix no auth callback always being executed

* allow opening server info from profile

closes sk22#593

* fix hide boosts icon color

closes sk22#676

* New translations strings.xml (Turkish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (German)

* New translations strings.xml (German)

* New translations strings.xml (Turkish)

* update fedinuke list

from source; doesn't contain any modifications regarding a recent issue

* New translations strings.xml (Turkish)

* remove unused class

* fix crash

* darken m3 outline color a bit

* use m3 outline again

* fix misalignment

closes sk22#682

* New translations strings.xml (Turkish)

* New translations full_description.txt (Turkish)

* New translations short_description.txt (Turkish)

* fix crash

* fix metadata sorting

* show pronouns in header/account lists

* fix broken divider line

closes sk22#679

* trim pronouns

* improve pronoun display

* New translations strings.xml (French)

* New translations strings.xml (Japanese)

* fix broken federated timeline

closes sk22#685

* fix broken -1 fallback behavior

closes sk22#681

* don't display nothing if server about request fails

closes sk22#678

* New translations strings.xml (Ukrainian)

* migrate global prefs to local prefs

* do confirm unfollow by default

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations full_description.txt (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Vietnamese)

* New translations full_description.txt (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* make sure list in prefs are always mutable and nut null

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Russian)

* fix pronouns edge case

* add back fix for stretched images

closes sk22#636

* fix null pointer on missing default posting language

* fix default posting language not being applied

* bigger username hitbox

closes sk22#688

* fix rtl header username alignment

closes sk22#689

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* hopefully fix crashes

closes sk22#692

* New translations strings.xml (Ukrainian)

* New translations full_description.txt (Ukrainian)

* fix pronoun crash

* New translations strings.xml (Persian)

* New translations strings.xml (Ukrainian)

* re-add true black mode

* asterisk can be a pronoun

* New translations strings.xml (Persian)

* true black mode fixes and clean-ups

* material 3 button background for switcher

* darker tab bar selected background

* better align follow/following button widths

* restore rainbow refresh colors

* fix search transition

* fix min width issue with switcher button

* fix no elevation when true black is enabled in light theme

* use statusForContent to determine spoilerRevealed

closes sk22#694

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* fix profile tab bar in true black theme

* fix m3 default button style

closes sk22#697

* prettier role badges

closes sk22#663

* fix translate button spacing

closes sk22#655

* use m3 switches in dialogs

closes sk22#653

* implement color palette switcher

* fix color palettes being overwritten

* add display and notification settings

* clean up code

* per-account single notification setting

* add missing items to notification types

* add prefix replies setting

* add show replies/boosts and reply visibility

* add load/see new posts settings

* fix spectator mode missing spoiler padding

* add a bunch of display settings

* update fedinuke

* add content type settings

* add settings for local-onlu

* add missing settings items

* fix visibility button icon tint

* hopefully fix some crashes

* normalize padding above edit text

* apparently, some people don't like pills

closes sk22#706

* fix play button color

closes sk22#705
This commit is contained in:
sk22
2023-07-16 18:01:42 +02:00
committed by GitHub
parent 3cfea0e660
commit 7677ad39ca
744 changed files with 24873 additions and 13485 deletions

View File

@@ -13,22 +13,20 @@ import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -43,6 +41,8 @@ public class CacheController{
private final String accountID;
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
private boolean loadingNotifications;
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
private static final int POST_FLAG_GAP_AFTER=1;
@@ -58,7 +58,6 @@ public class CacheController{
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
@@ -66,18 +65,16 @@ public class CacheController{
ArrayList<Status> result=new ArrayList<>();
cursor.moveToFirst();
String newMaxID;
outer:
do{
Status status=MastodonAPIController.gson.fromJson(cursor.getString(0), Status.class);
status.postprocess();
int flags=cursor.getInt(1);
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
newMaxID=status.id;
if (!new StatusFilterPredicate(filters, Filter.FilterContext.HOME).test(status))
continue outer;
result.add(status);
}while(cursor.moveToNext());
String _newMaxID=newMaxID;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
return;
}
@@ -85,11 +82,13 @@ public class CacheController{
Log.w(TAG, "getHomeTimeline: corrupted status object in database", x);
}
}
new GetHomeTimeline(maxID, null, count, null)
new GetHomeTimeline(maxID, null, count, null, AccountSessionManager.get(accountID).getLocalPreferences().timelineReplyVisibility)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(new StatusFilterPredicate(filters, Filter.FilterContext.HOME)).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
ArrayList<Status> filtered=new ArrayList<>(result);
AccountSessionManager.get(accountID).filterStatuses(filtered, FilterContext.HOME);
callback.onSuccess(new CacheablePaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id, false));
putHomeTimeline(result, maxID==null);
}
@@ -126,12 +125,39 @@ public class CacheController{
});
}
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<CacheablePaginatedResponse<List<Notification>>> callback){
public void updateStatus(Status status) {
runOnDbThread((db)->{
ContentValues statusUpdate=new ContentValues(1);
statusUpdate.put("json", MastodonAPIController.gson.toJson(status));
db.update("home_timeline", statusUpdate, "id = ?", new String[] { status.id });
});
}
public void updateNotification(Notification notification) {
runOnDbThread((db)->{
ContentValues notificationUpdate=new ContentValues(1);
notificationUpdate.put("json", MastodonAPIController.gson.toJson(notification));
String[] notificationArgs = new String[] { notification.id };
db.update("notifications_all", notificationUpdate, "id = ?", notificationArgs);
db.update("notifications_mentions", notificationUpdate, "id = ?", notificationArgs);
db.update("notifications_posts", notificationUpdate, "id = ?", notificationArgs);
ContentValues statusUpdate=new ContentValues(1);
statusUpdate.put("json", MastodonAPIController.gson.toJson(notification.status));
db.update("home_timeline", statusUpdate, "id = ?", new String[] { notification.status.id });
});
}
public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback<PaginatedResponse<List<Notification>>> callback){
cancelDelayedClose();
databaseThread.postRunnable(()->{
try{
AccountSession accountSession=AccountSessionManager.getInstance().getAccount(accountID);
List<Filter> filters=accountSession.wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
if(!onlyMentions && !onlyPosts && loadingNotifications){
synchronized(pendingNotificationsCallbacks){
pendingNotificationsCallbacks.add(callback);
}
return;
}
if(!forceReload){
SQLiteDatabase db=getOrOpenDatabase();
String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all";
@@ -140,42 +166,56 @@ public class CacheController{
ArrayList<Notification> result=new ArrayList<>();
cursor.moveToFirst();
String newMaxID;
outer:
do{
Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class);
ntf.postprocess();
newMaxID=ntf.id;
if(ntf.status!=null){
if (!new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status))
continue outer;
}
result.add(ntf);
}while(cursor.moveToNext());
String _newMaxID=newMaxID;
uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true)));
AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS);
uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID)));
return;
}
}catch(IOException x){
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
}
}
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.isAkkoma())
if(!onlyMentions && !onlyPosts)
loadingNotifications=true;
boolean isAkkoma = AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), isAkkoma)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){
callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{
if(ntf.status!=null){
return new StatusFilterPredicate(filters, Filter.FilterContext.NOTIFICATIONS).test(ntf.status);
}
return true;
}).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false));
ArrayList<Notification> filtered=new ArrayList<>(result);
AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS);
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id);
callback.onSuccess(res);
putNotifications(result, onlyMentions, onlyPosts, maxID==null);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
cb.onSuccess(res);
}
pendingNotificationsCallbacks.clear();
}
}
}
@Override
public void onError(ErrorResponse error){
callback.onError(error);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
cb.onError(error);
}
pendingNotificationsCallbacks.clear();
}
}
}
})
.exec(accountID);
@@ -327,7 +367,7 @@ public class CacheController{
createRecentSearchesTable(db);
}
if(oldVersion<3){
// MEGALODON-SPECIFIC
// MEGALODON
createPostsNotificationsTable(db);
}
if(oldVersion<4){

View File

@@ -117,6 +117,9 @@ public class MastodonAPIController{
synchronized(req){
req.okhttpCall=call;
}
if(req.timeout>0){
call.timeout().timeout(req.timeout, TimeUnit.MILLISECONDS);
}
if(BuildConfig.DEBUG)
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] Sending request: "+hreq);
@@ -153,13 +156,17 @@ public class MastodonAPIController{
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
if(req.respTypeToken!=null)
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
else
else if(req.respClass!=null)
respObj=gson.fromJson(respJson, req.respClass);
else
respObj=null;
}else{
if(req.respTypeToken!=null)
respObj=gson.fromJson(reader, req.respTypeToken.getType());
else
else if(req.respClass!=null)
respObj=gson.fromJson(reader, req.respClass);
else
respObj=null;
}
}catch(JsonIOException|JsonSyntaxException x){
if (req.context != null && response.body().contentType().subtype().equals("html")) {

View File

@@ -49,6 +49,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
Token token;
boolean canceled, isRemote;
Map<String, String> headers;
long timeout;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@Nullable Context context;
@@ -117,7 +118,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
.findAny())
.map(AccountSession::getID)
.map(this::exec)
.orElse(this.execNoAuth(domain));
.orElseGet(() -> this.execNoAuth(domain));
}
public MastodonAPIRequest<T> wrapProgress(Activity activity, @StringRes int message, boolean cancelable){
@@ -152,6 +153,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
headers.put(key, value);
}
protected void setTimeout(long timeout){
this.timeout=timeout;
}
protected String getPathPrefix(){
return "/api/v1";
}

View File

@@ -87,7 +87,6 @@ public class PushSubscriptionManager{
private String accountID;
private PrivateKey privateKey;
private PublicKey publicKey;
private PublicKey serverKey;
private byte[] authKey;
public PushSubscriptionManager(String accountID){
@@ -162,10 +161,6 @@ public class PushSubscriptionManager{
@Override
public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{
result.serverKey=result.serverKey.replace('/','_');
result.serverKey=result.serverKey.replace('+','-');
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
if(session==null)
return;

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.api;
import com.google.gson.reflect.TypeToken;
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
super(method, path, (Class<Void>)null);
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Hashtag;
import java.util.List;
public class GetAccountFeaturedHashtags extends MastodonAPIRequest<List<Hashtag>>{
public GetAccountFeaturedHashtags(String id){
super(HttpMethod.GET, "/accounts/"+id+"/featured_tags", new TypeToken<>(){});
}
}

View File

@@ -21,22 +21,22 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {}
case PINNED -> addQueryParameter("pinned", "true");
case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true");
addQueryParameter("exclude_reblogs", "true");
}
case OWN_POSTS_AND_REPLIES -> addQueryParameter("exclude_reblogs", "true");
case PINNED -> addQueryParameter("pinned", "true");
}
}
public enum Filter{
DEFAULT,
INCLUDE_REPLIES,
PINNED,
MEDIA,
NO_REBLOGS,
OWN_POSTS_AND_REPLIES
OWN_POSTS_AND_REPLIES,
PINNED
}
}

View File

@@ -4,21 +4,22 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<Token>{
public RegisterAccount(String username, String email, String password, String locale, String reason){
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
super(HttpMethod.POST, "/accounts", Token.class);
setRequestBody(new Body(username, email, password, locale, reason));
setRequestBody(new Body(username, email, password, locale, reason, timezone));
}
private static class Body{
public String username, email, password, locale, reason;
public String username, email, password, locale, reason, timeZone;
public boolean agreement=true;
public Body(String username, String email, String password, String locale, String reason){
public Body(String username, String email, String password, String locale, String reason, String timeZone){
this.username=username;
this.email=email;
this.password=password;
this.locale=locale;
this.reason=reason;
this.timeZone=timeZone;
}
}
}

View File

@@ -0,0 +1,34 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.StatusPrivacy;
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
}
private static class Request{
public Boolean locked, discoverable;
public RequestSource source;
public Request(Boolean locked, Boolean discoverable, RequestSource source){
this.locked=locked;
this.discoverable=discoverable;
this.source=source;
}
}
private static class RequestSource{
public StatusPrivacy privacy;
public String language;
public RequestSource(StatusPrivacy privacy, String language){
this.privacy=privacy;
this.language=language;
}
}
}

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.api.requests.catalog;
import android.net.Uri;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import java.util.List;
public class GetCatalogDefaultInstances extends MastodonAPIRequest<List<CatalogDefaultInstance>>{
public GetCatalogDefaultInstances(){
super(HttpMethod.GET, null, new TypeToken<>(){});
setTimeout(500);
}
@Override
public Uri getURL(){
return Uri.parse("https://api.joinmastodon.org/default-servers");
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors;
public class CreateFilter extends MastodonAPIRequest<Filter>{
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
super(HttpMethod.POST, "/filters", Filter.class);
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
public class DeleteFilter extends ResultlessMastodonAPIRequest{
public DeleteFilter(String id){
super(HttpMethod.DELETE, "/filters/"+id);
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import java.util.EnumSet;
import java.util.List;
class FilterRequest{
public String title;
public EnumSet<FilterContext> context;
public FilterAction filterAction;
public Integer expiresIn;
public List<KeywordAttribute> keywordsAttributes;
public FilterRequest(String title, EnumSet<FilterContext> context, FilterAction filterAction, Integer expiresIn, List<KeywordAttribute> keywordsAttributes){
this.title=title;
this.context=context;
this.filterAction=filterAction;
this.expiresIn=expiresIn;
this.keywordsAttributes=keywordsAttributes;
}
}

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.api.requests.accounts;
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.reflect.TypeToken;
@@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
import java.util.List;
public class GetWordFilters extends MastodonAPIRequest<List<Filter>>{
public GetWordFilters(){
public class GetFilters extends MastodonAPIRequest<List<Filter>>{
public GetFilters(){
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,14 @@
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.LegacyFilter;
import java.util.List;
public class GetLegacyFilters extends MastodonAPIRequest<List<LegacyFilter>>{
public GetLegacyFilters(){
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
}
}

View File

@@ -0,0 +1,18 @@
package org.joinmastodon.android.api.requests.filters;
import com.google.gson.annotations.SerializedName;
class KeywordAttribute{
public String id;
@SerializedName("_destroy")
public Boolean delete;
public String keyword;
public Boolean wholeWord;
public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
this.id=id;
this.delete=delete;
this.keyword=keyword;
this.wholeWord=wholeWord;
}
}

View File

@@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.filters;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class UpdateFilter extends MastodonAPIRequest<Filter>{
public UpdateFilter(String id, String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words, List<String> deletedWords){
super(HttpMethod.PUT, "/filters/"+id, Filter.class);
List<KeywordAttribute> attrs=Stream.of(
words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
).flatMap(Function.identity()).collect(Collectors.toList());
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -0,0 +1,16 @@
package org.joinmastodon.android.api.requests.instance;
import org.joinmastodon.android.api.MastodonAPIRequest;
import java.time.Instant;
public class GetInstanceExtendedDescription extends MastodonAPIRequest<GetInstanceExtendedDescription.Response>{
public GetInstanceExtendedDescription(){
super(HttpMethod.GET, "/instance/extended_description", Response.class);
}
public static class Response{
public Instant updatedAt;
public String content;
}
}

View File

@@ -1,17 +1,12 @@
package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.TimelineMarkers;
import java.util.EnumSet;
public class GetMarkers extends MastodonAPIRequest<Markers> {
public GetMarkers(EnumSet<Marker.Type> timelines) {
super(HttpMethod.GET, "/markers", Markers.class);
for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){
addQueryParameter("timeline[]", type);
}
public class GetMarkers extends MastodonAPIRequest<TimelineMarkers>{
public GetMarkers(){
super(HttpMethod.GET, "/markers", TimelineMarkers.class);
addQueryParameter("timeline[]", "home");
addQueryParameter("timeline[]", "notifications");
}
}

View File

@@ -2,11 +2,11 @@ package org.joinmastodon.android.api.requests.markers;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.TimelineMarkers;
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
public class SaveMarkers extends MastodonAPIRequest<TimelineMarkers>{
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
super(HttpMethod.POST, "/markers", Response.class);
super(HttpMethod.POST, "/markers", TimelineMarkers.class);
JsonObjectBuilder builder=new JsonObjectBuilder();
if(lastSeenHomePostID!=null)
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
@@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
setRequestBody(builder.build());
}
public static class Response{
public Marker home, notifications;
}
}

View File

@@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
addQueryParameter("resolve", "true");
}
public GetSearchResults limit(int limit){
addQueryParameter("limit", String.valueOf(limit));
return this;
}
@Override
protected String getPathPrefix(){
return "/api/v2";

View File

@@ -4,20 +4,19 @@ 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) {
public GetBubbleTimeline(String maxID, int limit, String replyVisibility) {
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);
if(replyVisibility != null)
addQueryParameter("reply_visibility", replyVisibility);
}
}

View File

@@ -2,16 +2,13 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import android.text.TextUtils;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit, List<String> containsAny, List<String> containsAll, List<String> containsNone, boolean localOnly){
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit, List<String> containsAny, List<String> containsAll, List<String> containsNone, boolean localOnly, String replyVisibility){
super(HttpMethod.GET, "/timelines/tag/"+hashtag, new TypeToken<>(){});
if (localOnly)
addQueryParameter("local", "true");
@@ -30,17 +27,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
if(containsNone!=null)
for (String tag : containsNone)
addQueryParameter("none[]", tag);
}
public GetHashtagTimeline(String hashtag, String maxID, String minID, int limit){
super(HttpMethod.GET, "/timelines/tag/"+hashtag, new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
if(replyVisibility != null)
addQueryParameter("reply_visibility", replyVisibility);
}
}

View File

@@ -2,14 +2,13 @@ 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;
import java.util.List;
public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID){
public GetHomeTimeline(String maxID, String minID, int limit, String sinceID, String replyVisibility){
super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
@@ -19,7 +18,7 @@ public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("since_id", sinceID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
if(replyVisibility != null)
addQueryParameter("reply_visibility", replyVisibility);
}
}

View File

@@ -2,14 +2,13 @@ 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;
import java.util.List;
public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID) {
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID, String replyVisibility) {
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
@@ -19,7 +18,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);
if(replyVisibility != null)
addQueryParameter("reply_visibility", replyVisibility);
}
}

View File

@@ -4,14 +4,13 @@ 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 GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit, String replyVisibility){
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
if(local)
addQueryParameter("local", "true");
@@ -21,7 +20,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);
if(replyVisibility != null)
addQueryParameter("reply_visibility", replyVisibility);
}
}

View File

@@ -0,0 +1,98 @@
package org.joinmastodon.android.api.session;
import static org.joinmastodon.android.GlobalUserPreferences.fromJson;
import static org.joinmastodon.android.GlobalUserPreferences.enumValue;
import static org.joinmastodon.android.api.MastodonAPIController.gson;
import android.content.SharedPreferences;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.TimelineDefinition;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class AccountLocalPreferences{
private final SharedPreferences prefs;
public boolean showInteractionCounts;
public boolean customEmojiInNames;
public boolean revealCWs;
public boolean hideSensitiveMedia;
public boolean serverSideFiltersSupported;
// MEGALODON
public boolean showReplies;
public boolean showBoosts;
public ArrayList<String> recentLanguages;
public boolean bottomEncoding;
public ContentType defaultContentType;
public boolean contentTypesEnabled;
public ArrayList<TimelineDefinition> timelines;
public boolean localOnlySupported;
public boolean glitchInstance;
public String publishButtonText;
public String timelineReplyVisibility; // akkoma-only
public boolean keepOnlyLatestNotification;
private final static Type recentLanguagesType = new TypeToken<ArrayList<String>>() {}.getType();
private final static Type timelinesType = new TypeToken<ArrayList<TimelineDefinition>>() {}.getType();
public AccountLocalPreferences(SharedPreferences prefs, AccountSession session){
this.prefs=prefs;
showInteractionCounts=prefs.getBoolean("interactionCounts", false);
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
revealCWs=prefs.getBoolean("revealCWs", false);
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
// MEGALODON
showReplies=prefs.getBoolean("showReplies", true);
showBoosts=prefs.getBoolean("showBoosts", true);
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new ArrayList<>());
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
defaultContentType=enumValue(ContentType.class, prefs.getString("defaultContentType", ContentType.PLAIN.name()));
contentTypesEnabled=prefs.getBoolean("contentTypesEnabled", true);
timelines=fromJson(prefs.getString("timelines", null), timelinesType, TimelineDefinition.getDefaultTimelines(session.getID()));
localOnlySupported=prefs.getBoolean("localOnlySupported", false);
glitchInstance=prefs.getBoolean("glitchInstance", false);
publishButtonText=prefs.getString("publishButtonText", null);
timelineReplyVisibility=prefs.getString("timelineReplyVisibility", null);
keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false);
}
public long getNotificationsPauseEndTime(){
return prefs.getLong("notificationsPauseTime", 0L);
}
public void setNotificationsPauseEndTime(long time){
prefs.edit().putLong("notificationsPauseTime", time).apply();
}
public void save(){
prefs.edit()
.putBoolean("interactionCounts", showInteractionCounts)
.putBoolean("emojiInNames", customEmojiInNames)
.putBoolean("revealCWs", revealCWs)
.putBoolean("hideSensitive", hideSensitiveMedia)
.putBoolean("serverSideFilters", serverSideFiltersSupported)
// MEGALODON
.putBoolean("showReplies", showReplies)
.putBoolean("showBoosts", showBoosts)
.putString("recentLanguages", gson.toJson(recentLanguages))
.putBoolean("bottomEncoding", bottomEncoding)
.putString("defaultContentType", defaultContentType==null ? null : defaultContentType.name())
.putBoolean("contentTypesEnabled", contentTypesEnabled)
.putString("timelines", gson.toJson(timelines))
.putBoolean("localOnlySupported", localOnlySupported)
.putBoolean("glitchInstance", glitchInstance)
.putString("publishButtonText", publishButtonText)
.putString("timelineReplyVisibility", timelineReplyVisibility)
.putBoolean("keepOnlyLatestNotification", keepOnlyLatestNotification)
.apply();
}
}

View File

@@ -1,25 +1,53 @@
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.Application;
import org.joinmastodon.android.model.Filter;
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.Markers;
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.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
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;
@@ -32,15 +60,17 @@ public class AccountSession{
public PushSubscription pushSubscription;
public boolean needUpdatePushSettings;
public long filtersLastUpdated;
public List<Filter> wordFilters=new ArrayList<>();
public List<LegacyFilter> wordFilters=new ArrayList<>();
public String pushAccountID;
public Preferences preferences;
public AccountActivationInfo activationInfo;
public Markers markers;
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;
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token;
@@ -58,10 +88,6 @@ public class AccountSession{
return domain+"_"+self.id;
}
public String getFullUsername() {
return "@"+self.username+"@"+domain;
}
public MastodonAPIController getApiController(){
if(apiController==null)
apiController=new MastodonAPIController(this);
@@ -92,6 +118,188 @@ public class AccountSession{
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);
}
})
.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, null, null)
.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);
}
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
Predicate<Status> statusIsOnOwnProfile = (s) -> self != null && profile != null && s.account != null
&& Objects.equals(self.id, profile.id) && Objects.equals(self.id, s.account.id);
if(getLocalPreferences().serverSideFiltersSupported){
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
if(s.filtered==null)
return false;
// don't hide own posts in own profile
if (statusIsOnOwnProfile.test(s))
return false;
for(FilterResult filter:s.filtered){
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
return true;
}
return false;
});
return;
}
if(wordFilters==null)
return;
for(T obj:objects){
Status s=extractor.apply(obj);
if(s!=null && s.filtered!=null){
getLocalPreferences().serverSideFiltersSupported=true;
getLocalPreferences().save();
return;
}
}
objects.removeIf(o->{
Status s=extractor.apply(o);
if(s==null)
return false;
// don't hide own posts in own profile
if (statusIsOnOwnProfile.test(s))
return false;
for(LegacyFilter filter:wordFilters){
if(filter.context.contains(context) && filter.matches(s) && filter.isActive())
return true;
}
return false;
});
}
public Optional<Instance> getInstance() {
return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain));
}

View File

@@ -21,23 +21,18 @@ import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.api.requests.markers.GetMarkers;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Marker;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token;
import java.io.File;
@@ -50,10 +45,10 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -155,11 +150,19 @@ public class AccountSessionManager{
return session;
}
public static AccountSession get(String id){
return getInstance().getAccount(id);
}
@Nullable
public AccountSession tryGetAccount(String id){
return sessions.get(id);
}
public static Optional<AccountSession> getOptional(String id) {
return Optional.ofNullable(getInstance().tryGetAccount(id));
}
@Nullable
public AccountSession tryGetAccount(Account account) {
return sessions.get(account.getDomainFromURL() + "_" + account.id);
@@ -192,13 +195,19 @@ public class AccountSessionManager{
AccountSession session=getAccount(id);
session.getCacheController().closeDatabase();
MastodonApp.context.deleteDatabase(id+".db");
GlobalUserPreferences.removeAccount(id);
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
MastodonApp.context.deleteSharedPreferences(id);
}else{
new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete();
}
sessions.remove(id);
if(lastActiveAccountID.equals(id)){
if(sessions.isEmpty())
lastActiveAccountID=null;
else
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
}
writeAccountsFile();
String domain=session.domain.toLowerCase();
@@ -271,14 +280,13 @@ public class AccountSessionManager{
HashSet<String> domains=new HashSet<>();
for(AccountSession session:sessions.values()){
domains.add(session.domain.toLowerCase());
if(now-session.infoLastUpdated>24L*3600_000L || session == activeSession){
updateSessionPreferences(session);
if(session == activeSession || now-session.infoLastUpdated>24L*3600_000L){
session.reloadPreferences(null);
updateSessionLocalInfo(session);
}
if(now-session.filtersLastUpdated>3600_000L || session == activeSession){
if(session == activeSession || (session.getLocalPreferences().serverSideFiltersSupported && now-session.filtersLastUpdated>3600_000L)){
updateSessionWordFilters(session);
}
updateSessionMarkers(session);
}
if(loadedInstances){
maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null);
@@ -289,20 +297,12 @@ public class AccountSessionManager{
long now=System.currentTimeMillis();
for(String domain:domains){
Long lastUpdated=instancesLastUpdated.get(domain);
if(lastUpdated==null || now-lastUpdated>24L*3600_000L || domain.equals(activeDomain)){
if(domain.equals(activeDomain) || lastUpdated==null || now-lastUpdated>24L*3600_000L){
updateInstanceInfo(domain);
}
}
}
private void preferencesFromSource(AccountSession session, Account account) {
if (account != null && account.source != null && session.preferences != null) {
if (account.source.privacy != null)
session.preferences.postingDefaultVisibility = account.source.privacy;
if (account.source.language != null)
session.preferences.postingDefaultLanguage = account.source.language;
}
}
private void updateSessionLocalInfo(AccountSession session){
new GetOwnAccount()
@@ -311,39 +311,7 @@ public class AccountSessionManager{
public void onSuccess(Account result){
session.self=result;
session.infoLastUpdated=System.currentTimeMillis();
preferencesFromSource(session, result);
writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){}
})
.exec(session.getID());
}
private void updateSessionPreferences(AccountSession session){
new GetPreferences().setCallback(new Callback<>() {
@Override
public void onSuccess(Preferences preferences) {
session.preferences=preferences;
preferencesFromSource(session, session.self);
}
@Override
public void onError(ErrorResponse error) {
session.preferences = new Preferences();
preferencesFromSource(session, session.self);
}
}).exec(session.getID());
}
private void updateSessionWordFilters(AccountSession session){
new GetWordFilters()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Filter> result){
session.wordFilters=result;
session.filtersLastUpdated=System.currentTimeMillis();
session.preferencesFromAccountSource(result);
writeAccountsFile();
}
@@ -355,19 +323,22 @@ public class AccountSessionManager{
.exec(session.getID());
}
private void updateSessionMarkers(AccountSession session) {
new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() {
@Override
public void onSuccess(Markers markers) {
session.markers = markers;
writeAccountsFile();
}
private void updateSessionWordFilters(AccountSession session){
new GetLegacyFilters()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<LegacyFilter> result){
session.wordFilters=result;
session.filtersLastUpdated=System.currentTimeMillis();
writeAccountsFile();
}
@Override
public void onError(ErrorResponse error) {
@Override
public void onError(ErrorResponse error){
}
}).exec(session.getID());
}
})
.exec(session.getID());
}
public void updateInstanceInfo(String domain){