Add support for /api/v2/instance

This commit is contained in:
Grishka
2024-09-19 02:37:19 +03:00
parent edc03642cc
commit 80323f8236
17 changed files with 408 additions and 182 deletions

View File

@@ -1,10 +1,8 @@
package org.joinmastodon.android.test; package org.joinmastodon.android.test;
import android.app.Instrumentation;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.view.View; import android.view.View;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
@@ -14,7 +12,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity; import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstanceV2;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID; import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
@@ -22,6 +20,7 @@ import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment; import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV2;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.junit.Assert; import org.junit.Assert;
@@ -32,12 +31,9 @@ import org.parceler.Parcels;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.PerformException; import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController; import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction; import androidx.test.espresso.ViewAction;
@@ -47,19 +43,19 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.Screenshot; import androidx.test.runner.screenshot.Screenshot;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import okio.BufferedSink; import okio.BufferedSink;
import okio.Okio; import okio.Okio;
import okio.Sink;
import okio.Source; import okio.Source;
import static androidx.test.espresso.Espresso.*; import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.*; import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.*; import static androidx.test.espresso.matcher.ViewMatchers.Visibility;
import static androidx.test.espresso.matcher.ViewMatchers.*; import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@LargeTest @LargeTest
@@ -148,10 +144,10 @@ public class StoreScreenshotsGenerator{
takeScreenshot("Thread"); takeScreenshot("Thread");
Instance[] _instance={null}; Instance[] _instance={null};
new GetInstance() new GetInstanceV2()
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance result){ public void onSuccess(InstanceV2 result){
_instance[0]=result; _instance[0]=result;
try{ try{
barrier.await(); barrier.await();

View File

@@ -6,7 +6,6 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
@@ -79,7 +78,7 @@ public class OAuthActivity extends Activity{
progress.dismiss(); progress.dismiss();
} }
}) })
.exec(instance.uri, token); .exec(instance.getDomain(), token);
} }
@Override @Override
@@ -88,7 +87,7 @@ public class OAuthActivity extends Activity{
progress.dismiss(); progress.dismiss();
} }
}) })
.execNoAuth(instance.uri); .execNoAuth(instance.getDomain());
} }
private void handleError(ErrorResponse error){ private void handleError(ErrorResponse error){

View File

@@ -0,0 +1,21 @@
package org.joinmastodon.android.api;
import me.grishka.appkit.api.APIRequest;
/**
* Wraps a different API request to allow a chain of requests to be canceled
*/
public class WrapperRequest<T> extends APIRequest<T>{
public APIRequest<?> wrappedRequest;
@Override
public void cancel(){
if(wrappedRequest!=null)
wrappedRequest.cancel();
}
@Override
public APIRequest<T> exec(){
throw new UnsupportedOperationException();
}
}

View File

@@ -1,10 +0,0 @@
package org.joinmastodon.android.api.requests.instance;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Instance;
public class GetInstance extends MastodonAPIRequest<Instance>{
public GetInstance(){
super(HttpMethod.GET, "/instance", Instance.class);
}
}

View File

@@ -0,0 +1,10 @@
package org.joinmastodon.android.api.requests.instance;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.InstanceV1;
public class GetInstanceV1 extends MastodonAPIRequest<InstanceV1>{
public GetInstanceV1(){
super(HttpMethod.GET, "/instance", InstanceV1.class);
}
}

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.api.requests.instance;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.InstanceV2;
public class GetInstanceV2 extends MastodonAPIRequest<InstanceV2>{
public GetInstanceV2(){
super(HttpMethod.GET, "/instance", InstanceV2.class);
}
@Override
protected String getPathPrefix(){
return "/api/v2";
}
}

View File

@@ -32,12 +32,15 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.DatabaseRunnable; import org.joinmastodon.android.api.DatabaseRunnable;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.WrapperRequest;
import org.joinmastodon.android.api.gson.JsonObjectBuilder; import org.joinmastodon.android.api.gson.JsonObjectBuilder;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters; import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstanceV1;
import org.joinmastodon.android.api.requests.instance.GetInstanceV2;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent; import org.joinmastodon.android.events.EmojiUpdatedEvent;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
@@ -45,6 +48,8 @@ import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV1;
import org.joinmastodon.android.model.InstanceV2;
import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
@@ -67,6 +72,7 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
@@ -74,7 +80,7 @@ public class AccountSessionManager{
private static final String TAG="AccountSessionManager"; private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push"; public static final String SCOPE="read write follow push";
public static final String REDIRECT_URI="mastodon-android-auth://callback"; public static final String REDIRECT_URI="mastodon-android-auth://callback";
private static final int DB_VERSION=2; private static final int DB_VERSION=3;
private static final AccountSessionManager instance=new AccountSessionManager(); private static final AccountSessionManager instance=new AccountSessionManager();
@@ -116,8 +122,8 @@ public class AccountSessionManager{
} }
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){ public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
instances.put(instance.uri, instance); instances.put(instance.getDomain(), instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo); AccountSession session=new AccountSession(token, self, app, instance.getDomain(), activationInfo==null, activationInfo);
sessions.put(session.getID(), session); sessions.put(session.getID(), session);
lastActiveAccountID=session.getID(); lastActiveAccountID=session.getID();
runOnDbThread(db->{ runOnDbThread(db->{
@@ -125,7 +131,7 @@ public class AccountSessionManager{
session.toContentValues(values); session.toContentValues(values);
db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE); db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE);
}); });
updateInstanceEmojis(instance, instance.uri); updateInstanceEmojis(instance, instance.getDomain());
if(PushSubscriptionManager.arePushNotificationsAvailable()){ if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null); session.getPushSubscriptionManager().registerAccountForPush(null);
} }
@@ -224,7 +230,7 @@ public class AccountSessionManager{
authenticatingApp=result; authenticatingApp=result;
Uri uri=new Uri.Builder() Uri uri=new Uri.Builder()
.scheme("https") .scheme("https")
.authority(instance.uri) .authority(instance.getDomain())
.path("/oauth/authorize") .path("/oauth/authorize")
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.appendQueryParameter("client_id", result.clientId) .appendQueryParameter("client_id", result.clientId)
@@ -245,7 +251,7 @@ public class AccountSessionManager{
} }
}) })
.wrapProgress(activity, R.string.preparing_auth, false) .wrapProgress(activity, R.string.preparing_auth, false)
.execNoAuth(instance.uri); .execNoAuth(instance.getDomain());
} }
public boolean isSelf(String id, Account other){ public boolean isSelf(String id, Account other){
@@ -337,8 +343,7 @@ public class AccountSessionManager{
} }
public void updateInstanceInfo(String domain){ public void updateInstanceInfo(String domain){
new GetInstance() loadInstanceInfo(domain, new Callback<>(){
.setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance instance){ public void onSuccess(Instance instance){
instances.put(domain, instance); instances.put(domain, instance);
@@ -349,8 +354,7 @@ public class AccountSessionManager{
public void onError(ErrorResponse error){ public void onError(ErrorResponse error){
} }
}) });
.execNoAuth(domain);
} }
private void updateInstanceEmojis(Instance instance, String domain){ private void updateInstanceEmojis(Instance instance, String domain){
@@ -379,7 +383,12 @@ public class AccountSessionManager{
while(cursor.moveToNext()){ while(cursor.moveToNext()){
DatabaseUtils.cursorRowToContentValues(cursor, values); DatabaseUtils.cursorRowToContentValues(cursor, values);
String domain=values.getAsString("domain"); String domain=values.getAsString("domain");
Instance instance=MastodonAPIController.gson.fromJson(values.getAsString("instance_obj"), Instance.class); int version=values.getAsInteger("version");
Instance instance=MastodonAPIController.gson.fromJson(values.getAsString("instance_obj"), switch(version){
case 1 -> InstanceV1.class;
case 2 -> InstanceV2.class;
default -> throw new IllegalStateException("Unexpected value: " + version);
});
List<Emoji> emojis=MastodonAPIController.gson.fromJson(values.getAsString("emojis"), new TypeToken<List<Emoji>>(){}.getType()); List<Emoji> emojis=MastodonAPIController.gson.fromJson(values.getAsString("emojis"), new TypeToken<List<Emoji>>(){}.getType());
instances.put(domain, instance); instances.put(domain, instance);
customEmojis.put(domain, groupCustomEmojis(emojis)); customEmojis.put(domain, groupCustomEmojis(emojis));
@@ -575,9 +584,49 @@ public class AccountSessionManager{
values.put("instance_obj", MastodonAPIController.gson.toJson(instance)); values.put("instance_obj", MastodonAPIController.gson.toJson(instance));
values.put("emojis", MastodonAPIController.gson.toJson(emojis)); values.put("emojis", MastodonAPIController.gson.toJson(emojis));
values.put("last_updated", lastUpdated); values.put("last_updated", lastUpdated);
values.put("version", instance.getVersion());
db.insertWithOnConflict("instances", null, values, SQLiteDatabase.CONFLICT_REPLACE); db.insertWithOnConflict("instances", null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
public static APIRequest<Instance> loadInstanceInfo(String domain, Callback<Instance> callback){
final WrapperRequest<Instance> wrapper=new WrapperRequest<>();
wrapper.wrappedRequest=new GetInstanceV2()
.setCallback(new Callback<>(){
@Override
public void onSuccess(InstanceV2 result){
wrapper.wrappedRequest=null;
callback.onSuccess(result);
}
@Override
public void onError(ErrorResponse error){
if(error instanceof MastodonErrorResponse mr && mr.httpStatus==404){
// Mastodon pre-4.0 or a non-Mastodon server altogether. Let's try /api/v1/instance
wrapper.wrappedRequest=new GetInstanceV1()
.setCallback(new Callback<>(){
@Override
public void onSuccess(InstanceV1 result){
wrapper.wrappedRequest=null;
callback.onSuccess(result);
}
@Override
public void onError(ErrorResponse error){
wrapper.wrappedRequest=null;
callback.onError(error);
}
})
.execNoAuth(domain);
}else{
wrapper.wrappedRequest=null;
callback.onError(error);
}
}
})
.execNoAuth(domain);
return wrapper;
}
private static class DatabaseHelper extends SQLiteOpenHelper{ private static class DatabaseHelper extends SQLiteOpenHelper{
public DatabaseHelper(){ public DatabaseHelper(){
super(MastodonApp.context, "accounts.db", null, DB_VERSION); super(MastodonApp.context, "accounts.db", null, DB_VERSION);
@@ -590,13 +639,28 @@ public class AccountSessionManager{
`id` text PRIMARY KEY, `id` text PRIMARY KEY,
`dismissed_at` bigint `dismissed_at` bigint
)"""); )""");
createAccountsAndInstancesTables(db); createAccountsTable(db);
db.execSQL("""
CREATE TABLE `instances` (
`domain` text PRIMARY KEY,
`instance_obj` text,
`emojis` text,
`last_updated` bigint,
`version` integer NOT NULL DEFAULT 1
)""");
} }
@Override @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
if(oldVersion<2){ if(oldVersion<2){
createAccountsAndInstancesTables(db); createAccountsTable(db);
db.execSQL("""
CREATE TABLE `instances` (
`domain` text PRIMARY KEY,
`instance_obj` text,
`emojis` text,
`last_updated` bigint
)""");
File accountsFile=new File(MastodonApp.context.getFilesDir(), "accounts.json"); File accountsFile=new File(MastodonApp.context.getFilesDir(), "accounts.json");
if(accountsFile.exists()){ if(accountsFile.exists()){
@@ -629,9 +693,12 @@ public class AccountSessionManager{
} }
} }
} }
if(oldVersion<3){
db.execSQL("ALTER TABLE `instances` ADD `version` integer NOT NULL DEFAULT 1");
}
} }
private void createAccountsAndInstancesTables(SQLiteDatabase db){ private void createAccountsTable(SQLiteDatabase db){
db.execSQL(""" db.execSQL("""
CREATE TABLE `accounts` ( CREATE TABLE `accounts` (
`id` text PRIMARY KEY, `id` text PRIMARY KEY,
@@ -648,13 +715,6 @@ public class AccountSessionManager{
`activation_info` text, `activation_info` text,
`preferences` text `preferences` text
)"""); )""");
db.execSQL("""
CREATE TABLE `instances` (
`domain` text PRIMARY KEY,
`instance_obj` text,
`emojis` text,
`last_updated` bigint
)""");
} }
} }
} }

View File

@@ -19,11 +19,13 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink; import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances; import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstanceV1;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment; import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment; import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment; import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV1;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance; import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect; import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -172,15 +174,14 @@ public class SplashFragment extends AppKitFragment{
} }
private void proceedWithServerDomain(String domain){ private void proceedWithServerDomain(String domain){
new GetInstance() AccountSessionManager.loadInstanceInfo(domain, new Callback<>(){
.setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance result){ public void onSuccess(Instance result){
if(getActivity()==null) if(getActivity()==null)
return; return;
instanceLoadingProgress.dismiss(); instanceLoadingProgress.dismiss();
instanceLoadingProgress=null; instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){ if(!result.areRegistrationsOpen() && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed) .setMessage(R.string.instance_signup_closed)
@@ -203,8 +204,7 @@ public class SplashFragment extends AppKitFragment{
instanceLoadingProgress=null; instanceLoadingProgress=null;
error.showToast(getActivity()); error.showToast(getActivity());
} }
}) });
.execNoAuth(domain);
} }
private void onLearnMoreClick(View v){ private void onLearnMoreClick(View v){

View File

@@ -1,9 +1,6 @@
package org.joinmastodon.android.fragments.onboarding; package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity; import android.app.Activity;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -11,14 +8,11 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener; import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@@ -26,7 +20,6 @@ import org.jsoup.nodes.Document;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
@@ -36,14 +29,10 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.ToolbarFragment; import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout; import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call; import okhttp3.Call;
@@ -99,7 +88,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false); View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text); TextView text=headerView.findViewById(R.id.text);
text.setText(getString(R.string.privacy_policy_subtitle, instance.uri)); text.setText(getString(R.string.privacy_policy_subtitle, instance.getDomain()));
adapter=new MergeRecyclerAdapter(); adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
@@ -111,7 +100,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
buttonBar=view.findViewById(R.id.button_bar); buttonBar=view.findViewById(R.id.button_bar);
Button backBtn=view.findViewById(R.id.btn_back); Button backBtn=view.findViewById(R.id.btn_back);
backBtn.setText(getString(R.string.server_policy_disagree, instance.uri)); backBtn.setText(getString(R.string.server_policy_disagree, instance.getDomain()));
backBtn.setOnClickListener(v->{ backBtn.setOnClickListener(v->{
setResult(false, null); setResult(false, null);
Nav.finish(this); Nav.finish(this);
@@ -159,7 +148,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private void loadServerPrivacyPolicy(){ private void loadServerPrivacyPolicy(){
Request req=new Request.Builder() Request req=new Request.Builder()
.url("https://"+instance.uri+"/terms") .url("https://"+instance.getDomain()+"/terms")
.addHeader("Accept-Language", Locale.getDefault().toLanguageTag()) .addHeader("Accept-Language", Locale.getDefault().toLanguageTag())
.build(); .build();
currentRequest=MastodonAPIController.getHttpClient().newCall(req); currentRequest=MastodonAPIController.getHttpClient().newCall(req);
@@ -176,7 +165,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
if(!response.isSuccessful()) if(!response.isSuccessful())
return; return;
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString()); Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
final Item item=new Item(doc.title(), null, instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico"); final Item item=new Item(doc.title(), null, instance.getDomain(), req.url().toString(), "https://"+instance.getDomain()+"/favicon.ico");
Activity activity=getActivity(); Activity activity=getActivity();
if(activity!=null){ if(activity!=null){
activity.runOnUiThread(()->{ activity.runOnUiThread(()->{

View File

@@ -15,8 +15,11 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.requests.instance.GetInstanceV1;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV1;
import org.joinmastodon.android.model.InstanceV2;
import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
@@ -43,6 +46,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.BaseRecyclerFragment;
@@ -63,7 +67,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected HashMap<String, Instance> instancesCache=new HashMap<>(); protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar; protected View buttonBar;
protected List<CatalogInstance> filteredData=new ArrayList<>(); protected List<CatalogInstance> filteredData=new ArrayList<>();
protected GetInstance loadingInstanceRequest; protected APIRequest<Instance> loadingInstanceRequest;
protected Call loadingInstanceRedirectRequest; protected Call loadingInstanceRedirectRequest;
protected ProgressDialog instanceProgressDialog; protected ProgressDialog instanceProgressDialog;
protected HashMap<String, String> redirects=new HashMap<>(); protected HashMap<String, String> redirects=new HashMap<>();
@@ -181,7 +185,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
showInstanceInfoLoadError(domain, x); showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){ if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error); fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){ if(!filteredData.isEmpty() && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){ if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
ivh.rebind(); ivh.rebind();
} }
@@ -190,13 +194,15 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return; return;
} }
loadingInstanceDomain=domain; loadingInstanceDomain=domain;
loadingInstanceRequest=new GetInstance(); loadingInstanceRequest=AccountSessionManager.loadInstanceInfo(domain, new Callback<>(){
loadingInstanceRequest.setCallback(new Callback<>(){
@Override @Override
public void onSuccess(Instance result){ public void onSuccess(Instance result){
loadingInstanceRequest=null; loadingInstanceRequest=null;
loadingInstanceDomain=null; loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection if(result instanceof InstanceV1 v1)
v1.uri=domain; // needed for instances that use domain redirection
else if(result instanceof InstanceV2 v2)
v2.domain=domain;
instancesCache.put(domain, result); instancesCache.put(domain, result);
if(instanceProgressDialog!=null || onError!=null) if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result); proceedWithAuthOrSignup(result);
@@ -246,7 +252,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
} }
} }
} }
}).execNoAuth(domain); });
} }
private void cancelLoadingInstanceInfo(){ private void cancelLoadingInstanceInfo(){

View File

@@ -330,7 +330,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
if(currentInviteLinkAlert!=null){ if(currentInviteLinkAlert!=null){
currentInviteLinkAlert.dismiss(); currentInviteLinkAlert.dismiss();
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.isValidInviteUrl(currentSearchQueryButWithCasePreserved)){ }else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.isValidInviteUrl(currentSearchQueryButWithCasePreserved)){
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){ if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.getDomain(), inviteCodeHost)){
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved); Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
new CheckInviteLink(inviteLink.getPath()) new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@@ -368,9 +368,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
} }
} }
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0); getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){ if(!instance.areRegistrationsOpen() && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.getDomain(), inviteCodeHost))){
if(instance.invitesEnabled){ if(instance.areInvitesEnabled()){
showInviteLinkAlert(instance.uri); showInviteLinkAlert(instance.getDomain());
}else{ }else{
new M3AlertDialogBuilder(getActivity()) new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error) .setTitle(R.string.error)
@@ -382,7 +382,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
} }
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); args.putParcelable("instance", Parcels.wrap(instance));
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost)) if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.getDomain(), inviteCodeHost))
args.putString("inviteCode", inviteCode); args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args); Nav.go(getActivity(), InstanceRulesFragment.class, args);
} }
@@ -667,7 +667,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
radioButton.setChecked(chosenInstance==item); radioButton.setChecked(chosenInstance==item);
Instance realInstance=instancesCache.get(item.normalizedDomain); Instance realInstance=instancesCache.get(item.normalizedDomain);
float alpha; float alpha;
if(realInstance!=null && !realInstance.registrations){ if(realInstance!=null && !realInstance.areRegistrationsOpen()){
alpha=0.38f; alpha=0.38f;
description.setText(R.string.not_accepting_new_members); description.setText(R.string.not_accepting_new_members);
enabled=false; enabled=false;

View File

@@ -12,7 +12,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter; import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener; import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -58,7 +57,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false); View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text); TextView text=headerView.findViewById(R.id.text);
text.setText(Html.fromHtml(getString(R.string.instance_rules_subtitle, "<b>"+Html.escapeHtml(instance.uri)+"</b>"))); text.setText(Html.fromHtml(getString(R.string.instance_rules_subtitle, "<b>"+Html.escapeHtml(instance.getDomain())+"</b>")));
adapter=new MergeRecyclerAdapter(); adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));

View File

@@ -115,7 +115,7 @@ public class SignupFragment extends ToolbarFragment{
passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap); passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap);
reasonWrap=view.findViewById(R.id.reason_wrap); reasonWrap=view.findViewById(R.id.reason_wrap);
domain.setText('@'+instance.uri); domain.setText('@'+instance.getDomain());
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override @Override
@@ -143,7 +143,7 @@ public class SignupFragment extends ToolbarFragment{
passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm)); passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm));
reason.addTextChangedListener(new ErrorClearingListener(reason)); reason.addTextChangedListener(new ErrorClearingListener(reason));
if(!instance.approvalRequired){ if(!instance.isApprovalRequired()){
reason.setVisibility(View.GONE); reason.setVisibility(View.GONE);
reasonExplain.setVisibility(View.GONE); reasonExplain.setVisibility(View.GONE);
} }
@@ -285,7 +285,7 @@ public class SignupFragment extends ToolbarFragment{
progressDialog.dismiss(); progressDialog.dismiss();
} }
}) })
.exec(instance.uri, apiToken); .exec(instance.getDomain(), apiToken);
} }
private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){ private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){
@@ -317,7 +317,7 @@ public class SignupFragment extends ToolbarFragment{
case "email" -> switch(error.error){ case "email" -> switch(error.error){
case "ERR_BLOCKED" -> { case "ERR_BLOCKED" -> {
String emailAddr=email.getText().toString(); String emailAddr=email.getText().toString();
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1))); String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.getDomain()), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
yield makeLinkInErrorMessage(s, this::onGoBackLinkClick); yield makeLinkInErrorMessage(s, this::onGoBackLinkClick);
} }
case "ERR_INVALID" -> getString(R.string.signup_email_invalid); case "ERR_INVALID" -> getString(R.string.signup_email_invalid);
@@ -364,7 +364,7 @@ public class SignupFragment extends ToolbarFragment{
private void updateButtonState(){ private void updateButtonState(){
btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find() btn.setEnabled(username.length()>0 && email.length()>0 && emailRegex.matcher(email.getText()).find()
&& password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString()) && password.length()>=8 && passwordConfirm.length()>=8 && password.getText().toString().equals(passwordConfirm.getText().toString())
&& (!instance.approvalRequired || reason.length()>0)); && (!instance.isApprovalRequired() || reason.length()>0));
} }
private void createAppAndGetToken(){ private void createAppAndGetToken(){
@@ -386,7 +386,7 @@ public class SignupFragment extends ToolbarFragment{
} }
} }
}) })
.execNoAuth(instance.uri); .execNoAuth(instance.getDomain());
} }
private void getToken(){ private void getToken(){
@@ -412,7 +412,7 @@ public class SignupFragment extends ToolbarFragment{
} }
} }
}) })
.execNoAuth(instance.uri); .execNoAuth(instance.getDomain());
} }
@Override @Override
@@ -426,7 +426,7 @@ public class SignupFragment extends ToolbarFragment{
} }
private void onForgotPasswordLinkClick(LinkSpan span){ private void onForgotPasswordLinkClick(LinkSpan span){
UiUtils.launchWebBrowser(getActivity(), "https://"+instance.uri+"/auth/password/new"); UiUtils.launchWebBrowser(getActivity(), "https://"+instance.getDomain()+"/auth/password/new");
} }
private void onPasswordFieldFocusChange(View v, boolean hasFocus){ private void onPasswordFieldFocusChange(View v, boolean hasFocus){

View File

@@ -100,13 +100,13 @@ public class SettingsServerAboutFragment extends LoaderFragment{
scroller.setClipToPadding(false); scroller.setClipToPadding(false);
scroller.addView(scrollingLayout); scroller.addView(scrollingLayout);
if(!TextUtils.isEmpty(instance.thumbnail)){ if(!TextUtils.isEmpty(instance.getThumbnailURL())){
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity()); FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f); banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP); banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16)); banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true); banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail)); ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.getThumbnailURL()));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24); blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp); scrollingLayout.addView(banner, blp);
@@ -115,7 +115,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
} }
boolean needDivider=false; boolean needDivider=false;
if(instance.contactAccount!=null){ if(instance.getContactAccount()!=null){
needDivider=true; needDivider=true;
TextView heading=new TextView(getActivity()); TextView heading=new TextView(getActivity());
heading.setTextAppearance(R.style.m3_title_small); heading.setTextAppearance(R.style.m3_title_small);
@@ -128,7 +128,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
hlp.leftMargin=hlp.rightMargin=V.dp(16); hlp.leftMargin=hlp.rightMargin=V.dp(16);
scrollingLayout.addView(heading, hlp); scrollingLayout.addView(heading, hlp);
AccountViewModel model=new AccountViewModel(instance.contactAccount, accountID); AccountViewModel model=new AccountViewModel(instance.getContactAccount(), accountID);
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null); AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false); holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
holder.bind(model); holder.bind(model);
@@ -140,7 +140,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, i+1), null, model.emojiHelper.getImageRequest(i), false); ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, i+1), null, model.emojiHelper.getImageRequest(i), false);
} }
} }
if(!TextUtils.isEmpty(instance.email)){ if(!TextUtils.isEmpty(instance.getContactEmail())){
needDivider=true; needDivider=true;
SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout); SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout);
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, i->{}); ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, i->{});
@@ -208,7 +208,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
public void onRefresh(){} public void onRefresh(){}
private void openAdminEmail(){ private void openAdminEmail(){
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.email, null)); Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.getContactEmail(), null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try{ try{
startActivity(intent); startActivity(intent);

View File

@@ -1,7 +1,5 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import android.text.Html;
import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.catalog.CatalogInstance; import org.joinmastodon.android.model.catalog.CatalogInstance;
@@ -10,15 +8,8 @@ import org.parceler.Parcel;
import java.net.IDN; import java.net.IDN;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
@Parcel public abstract class Instance extends BaseModel{
public class Instance extends BaseModel{
/**
* The domain name of the instance.
*/
@RequiredField
public String uri;
/** /**
* The title of the website. * The title of the website.
*/ */
@@ -29,16 +20,6 @@ public class Instance extends BaseModel{
*/ */
@RequiredField @RequiredField
public String description; public String description;
/**
* A shorter description defined by the admin.
*/
// @RequiredField
public String shortDescription;
/**
* An email that may be contacted for any inquiries.
*/
@RequiredField
public String email;
/** /**
* The version of Mastodon installed on the instance. * The version of Mastodon installed on the instance.
*/ */
@@ -49,32 +30,7 @@ public class Instance extends BaseModel{
*/ */
// @RequiredField // @RequiredField
public List<String> languages; public List<String> languages;
/**
* Whether registrations are enabled.
*/
public boolean registrations;
/**
* Whether registrations require moderator approval.
*/
public boolean approvalRequired;
/**
* Whether invites are enabled.
*/
public boolean invitesEnabled;
/**
* URLs of interest for clients apps.
*/
public Map<String, String> urls;
/**
* Banner image for the website.
*/
public String thumbnail;
/**
* A user that can be contacted, as an alternative to email.
*/
public Account contactAccount;
public Stats stats;
public List<Rule> rules; public List<Rule> rules;
public Configuration configuration; public Configuration configuration;
@@ -85,51 +41,38 @@ public class Instance extends BaseModel{
@Override @Override
public void postprocess() throws ObjectValidationException{ public void postprocess() throws ObjectValidationException{
super.postprocess(); super.postprocess();
if(contactAccount!=null)
contactAccount.postprocess();
if(rules==null) if(rules==null)
rules=Collections.emptyList(); rules=Collections.emptyList();
if(shortDescription==null)
shortDescription="";
}
@Override
public String toString(){
return "Instance{"+
"uri='"+uri+'\''+
", title='"+title+'\''+
", description='"+description+'\''+
", shortDescription='"+shortDescription+'\''+
", email='"+email+'\''+
", version='"+version+'\''+
", languages="+languages+
", registrations="+registrations+
", approvalRequired="+approvalRequired+
", invitesEnabled="+invitesEnabled+
", urls="+urls+
", thumbnail='"+thumbnail+'\''+
", contactAccount="+contactAccount+
'}';
} }
public CatalogInstance toCatalogInstance(){ public CatalogInstance toCatalogInstance(){
CatalogInstance ci=new CatalogInstance(); CatalogInstance ci=new CatalogInstance();
ci.domain=uri; ci.domain=getDomain();
ci.normalizedDomain=IDN.toUnicode(uri); ci.normalizedDomain=IDN.toUnicode(getDomain());
ci.description=Html.fromHtml(shortDescription).toString().trim(); ci.description=description.trim();
if(languages!=null&&languages.size()>0){ if(languages!=null && !languages.isEmpty()){
ci.language=languages.get(0); ci.language=languages.get(0);
ci.languages=languages; ci.languages=languages;
}else{ }else{
ci.languages=Collections.emptyList(); ci.languages=List.of();
ci.language="unknown"; ci.language="unknown";
} }
ci.proxiedThumbnail=thumbnail; ci.proxiedThumbnail=getThumbnailURL();
if(stats!=null) // if(stats!=null)
ci.totalUsers=stats.userCount; // ci.totalUsers=stats.userCount;
return ci; return ci;
} }
public abstract String getDomain();
public abstract Account getContactAccount();
public abstract String getContactEmail();
public abstract boolean areRegistrationsOpen();
public abstract boolean isApprovalRequired();
public abstract boolean areInvitesEnabled();
public abstract String getThumbnailURL();
public abstract int getVersion();
public abstract long getApiVersion(String name);
@Parcel @Parcel
public static class Rule{ public static class Rule{
public String id; public String id;
@@ -138,13 +81,6 @@ public class Instance extends BaseModel{
public transient CharSequence parsedText; public transient CharSequence parsedText;
} }
@Parcel
public static class Stats{
public int userCount;
public int statusCount;
public int domainCount;
}
@Parcel @Parcel
public static class Configuration{ public static class Configuration{
public StatusesConfiguration statuses; public StatusesConfiguration statuses;

View File

@@ -0,0 +1,112 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.util.Map;
@Parcel
public class InstanceV1 extends Instance{
/**
* The domain name of the instance.
*/
@RequiredField
public String uri;
/**
* A shorter description defined by the admin.
*/
// @RequiredField
public String shortDescription;
/**
* An email that may be contacted for any inquiries.
*/
@RequiredField
public String email;
/**
* Whether registrations are enabled.
*/
public boolean registrations;
/**
* Whether registrations require moderator approval.
*/
public boolean approvalRequired;
/**
* Whether invites are enabled.
*/
public boolean invitesEnabled;
/**
* URLs of interest for clients apps.
*/
public Map<String, String> urls;
/**
* A user that can be contacted, as an alternative to email.
*/
public Account contactAccount;
public Stats stats;
/**
* Banner image for the website.
*/
public String thumbnail;
@Override
public String getDomain(){
return uri;
}
@Override
public Account getContactAccount(){
return contactAccount;
}
@Override
public String getContactEmail(){
return email;
}
@Override
public boolean areInvitesEnabled(){
return invitesEnabled;
}
@Override
public boolean areRegistrationsOpen(){
return registrations;
}
@Override
public boolean isApprovalRequired(){
return approvalRequired;
}
@Override
public String getThumbnailURL(){
return thumbnail;
}
@Override
public int getVersion(){
return 1;
}
@Override
public long getApiVersion(String name){
return 0;
}
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
if(shortDescription==null)
shortDescription="";
if(contactAccount!=null)
contactAccount.postprocess();
}
@Parcel
public static class Stats{
public int userCount;
public int statusCount;
public int domainCount;
}
}

View File

@@ -0,0 +1,93 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.util.Map;
@Parcel
public class InstanceV2 extends Instance{
@RequiredField
public String domain;
public Thumbnail thumbnail;
@RequiredField
public Registrations registrations;
public Contact contact;
public Map<String, Long> apiVersions;
@Override
public String getDomain(){
return domain;
}
@Override
public Account getContactAccount(){
return contact!=null ? contact.account : null;
}
@Override
public String getContactEmail(){
return contact!=null ? contact.email : null;
}
@Override
public boolean areRegistrationsOpen(){
return registrations.enabled;
}
@Override
public boolean isApprovalRequired(){
return registrations.approvalRequired;
}
@Override
public boolean areInvitesEnabled(){
return true; // TODO are they though?
}
@Override
public String getThumbnailURL(){
return thumbnail!=null ? thumbnail.url : null;
}
@Override
public int getVersion(){
return 2;
}
@Override
public long getApiVersion(String name){
if(apiVersions==null)
return 0;
Long v=apiVersions.get(name);
return v==null ? 0 : v;
}
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
if(contact!=null && contact.account!=null)
contact.account.postprocess();
}
@Parcel
public static class Thumbnail{
public String url;
public String blurhash;
}
@Parcel
public static class Registrations{
public boolean enabled;
public boolean approvalRequired;
public String message;
public String url;
}
@Parcel
public static class Contact{
public String email;
public Account account;
}
}