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;
import android.app.Instrumentation;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -14,7 +12,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
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.session.AccountSession;
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.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV2;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.junit.Assert;
@@ -32,12 +31,9 @@ import org.parceler.Parcels;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeoutException;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
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.filters.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.Screenshot;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import okio.BufferedSink;
import okio.Okio;
import okio.Sink;
import okio.Source;
import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.assertion.ViewAssertions.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility;
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)
@LargeTest
@@ -148,10 +144,10 @@ public class StoreScreenshotsGenerator{
takeScreenshot("Thread");
Instance[] _instance={null};
new GetInstance()
new GetInstanceV2()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
public void onSuccess(InstanceV2 result){
_instance[0]=result;
try{
barrier.await();

View File

@@ -6,7 +6,6 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
@@ -79,7 +78,7 @@ public class OAuthActivity extends Activity{
progress.dismiss();
}
})
.exec(instance.uri, token);
.exec(instance.getDomain(), token);
}
@Override
@@ -88,7 +87,7 @@ public class OAuthActivity extends Activity{
progress.dismiss();
}
})
.execNoAuth(instance.uri);
.execNoAuth(instance.getDomain());
}
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.DatabaseRunnable;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.WrapperRequest;
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
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.events.EmojiUpdatedEvent;
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.EmojiCategory;
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.Preferences;
import org.joinmastodon.android.model.Token;
@@ -67,6 +72,7 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -74,7 +80,7 @@ public class AccountSessionManager{
private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push";
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();
@@ -116,8 +122,8 @@ public class AccountSessionManager{
}
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
instances.put(instance.uri, instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
instances.put(instance.getDomain(), instance);
AccountSession session=new AccountSession(token, self, app, instance.getDomain(), activationInfo==null, activationInfo);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
runOnDbThread(db->{
@@ -125,7 +131,7 @@ public class AccountSessionManager{
session.toContentValues(values);
db.insertWithOnConflict("accounts", null, values, SQLiteDatabase.CONFLICT_REPLACE);
});
updateInstanceEmojis(instance, instance.uri);
updateInstanceEmojis(instance, instance.getDomain());
if(PushSubscriptionManager.arePushNotificationsAvailable()){
session.getPushSubscriptionManager().registerAccountForPush(null);
}
@@ -224,7 +230,7 @@ public class AccountSessionManager{
authenticatingApp=result;
Uri uri=new Uri.Builder()
.scheme("https")
.authority(instance.uri)
.authority(instance.getDomain())
.path("/oauth/authorize")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("client_id", result.clientId)
@@ -245,7 +251,7 @@ public class AccountSessionManager{
}
})
.wrapProgress(activity, R.string.preparing_auth, false)
.execNoAuth(instance.uri);
.execNoAuth(instance.getDomain());
}
public boolean isSelf(String id, Account other){
@@ -337,8 +343,7 @@ public class AccountSessionManager{
}
public void updateInstanceInfo(String domain){
new GetInstance()
.setCallback(new Callback<>(){
loadInstanceInfo(domain, new Callback<>(){
@Override
public void onSuccess(Instance instance){
instances.put(domain, instance);
@@ -349,8 +354,7 @@ public class AccountSessionManager{
public void onError(ErrorResponse error){
}
})
.execNoAuth(domain);
});
}
private void updateInstanceEmojis(Instance instance, String domain){
@@ -379,7 +383,12 @@ public class AccountSessionManager{
while(cursor.moveToNext()){
DatabaseUtils.cursorRowToContentValues(cursor, values);
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());
instances.put(domain, instance);
customEmojis.put(domain, groupCustomEmojis(emojis));
@@ -575,9 +584,49 @@ public class AccountSessionManager{
values.put("instance_obj", MastodonAPIController.gson.toJson(instance));
values.put("emojis", MastodonAPIController.gson.toJson(emojis));
values.put("last_updated", lastUpdated);
values.put("version", instance.getVersion());
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{
public DatabaseHelper(){
super(MastodonApp.context, "accounts.db", null, DB_VERSION);
@@ -590,13 +639,28 @@ public class AccountSessionManager{
`id` text PRIMARY KEY,
`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
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
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");
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("""
CREATE TABLE `accounts` (
`id` text PRIMARY KEY,
@@ -648,13 +715,6 @@ public class AccountSessionManager{
`activation_info` 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.requests.accounts.CheckInviteLink;
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.InstanceChooserLoginFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.InstanceV1;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -172,15 +174,14 @@ public class SplashFragment extends AppKitFragment{
}
private void proceedWithServerDomain(String domain){
new GetInstance()
.setCallback(new Callback<>(){
AccountSessionManager.loadInstanceInfo(domain, new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
if(!result.areRegistrationsOpen() && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
@@ -203,8 +204,7 @@ public class SplashFragment extends AppKitFragment{
instanceLoadingProgress=null;
error.showToast(getActivity());
}
})
.execNoAuth(domain);
});
}
private void onLearnMoreClick(View v){

View File

@@ -1,9 +1,6 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -11,14 +8,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
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.utils.ElevationOnScrollListener;
import org.jsoup.Jsoup;
@@ -26,7 +20,6 @@ import org.jsoup.nodes.Document;
import org.parceler.Parcels;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Locale;
@@ -36,14 +29,10 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
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.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
@@ -99,7 +88,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
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.addAdapter(new SingleViewRecyclerAdapter(headerView));
@@ -111,7 +100,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
buttonBar=view.findViewById(R.id.button_bar);
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->{
setResult(false, null);
Nav.finish(this);
@@ -159,7 +148,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private void loadServerPrivacyPolicy(){
Request req=new Request.Builder()
.url("https://"+instance.uri+"/terms")
.url("https://"+instance.getDomain()+"/terms")
.addHeader("Accept-Language", Locale.getDefault().toLanguageTag())
.build();
currentRequest=MastodonAPIController.getHttpClient().newCall(req);
@@ -176,7 +165,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
if(!response.isSuccessful())
return;
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();
if(activity!=null){
activity.runOnUiThread(()->{

View File

@@ -15,8 +15,11 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
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.InstanceV1;
import org.joinmastodon.android.model.InstanceV2;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -43,6 +46,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
@@ -63,7 +67,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar;
protected List<CatalogInstance> filteredData=new ArrayList<>();
protected GetInstance loadingInstanceRequest;
protected APIRequest<Instance> loadingInstanceRequest;
protected Call loadingInstanceRedirectRequest;
protected ProgressDialog instanceProgressDialog;
protected HashMap<String, String> redirects=new HashMap<>();
@@ -181,7 +185,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){
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){
ivh.rebind();
}
@@ -190,13 +194,15 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return;
}
loadingInstanceDomain=domain;
loadingInstanceRequest=new GetInstance();
loadingInstanceRequest.setCallback(new Callback<>(){
loadingInstanceRequest=AccountSessionManager.loadInstanceInfo(domain, new Callback<>(){
@Override
public void onSuccess(Instance result){
loadingInstanceRequest=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);
if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result);
@@ -246,7 +252,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
}
}
}).execNoAuth(domain);
});
}
private void cancelLoadingInstanceInfo(){

View File

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

View File

@@ -12,7 +12,6 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -58,7 +57,7 @@ public class InstanceRulesFragment extends ToolbarFragment{
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
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.addAdapter(new SingleViewRecyclerAdapter(headerView));

View File

@@ -115,7 +115,7 @@ public class SignupFragment extends ToolbarFragment{
passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap);
reasonWrap=view.findViewById(R.id.reason_wrap);
domain.setText('@'+instance.uri);
domain.setText('@'+instance.getDomain());
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
@@ -143,7 +143,7 @@ public class SignupFragment extends ToolbarFragment{
passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm));
reason.addTextChangedListener(new ErrorClearingListener(reason));
if(!instance.approvalRequired){
if(!instance.isApprovalRequired()){
reason.setVisibility(View.GONE);
reasonExplain.setVisibility(View.GONE);
}
@@ -285,7 +285,7 @@ public class SignupFragment extends ToolbarFragment{
progressDialog.dismiss();
}
})
.exec(instance.uri, apiToken);
.exec(instance.getDomain(), apiToken);
}
private CharSequence makeLinkInErrorMessage(String source, LinkSpan.OnLinkClickListener onClick){
@@ -317,7 +317,7 @@ public class SignupFragment extends ToolbarFragment{
case "email" -> switch(error.error){
case "ERR_BLOCKED" -> {
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);
}
case "ERR_INVALID" -> getString(R.string.signup_email_invalid);
@@ -364,7 +364,7 @@ public class SignupFragment extends ToolbarFragment{
private void updateButtonState(){
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())
&& (!instance.approvalRequired || reason.length()>0));
&& (!instance.isApprovalRequired() || reason.length()>0));
}
private void createAppAndGetToken(){
@@ -386,7 +386,7 @@ public class SignupFragment extends ToolbarFragment{
}
}
})
.execNoAuth(instance.uri);
.execNoAuth(instance.getDomain());
}
private void getToken(){
@@ -412,7 +412,7 @@ public class SignupFragment extends ToolbarFragment{
}
}
})
.execNoAuth(instance.uri);
.execNoAuth(instance.getDomain());
}
@Override
@@ -426,7 +426,7 @@ public class SignupFragment extends ToolbarFragment{
}
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){

View File

@@ -100,13 +100,13 @@ public class SettingsServerAboutFragment extends LoaderFragment{
scroller.setClipToPadding(false);
scroller.addView(scrollingLayout);
if(!TextUtils.isEmpty(instance.thumbnail)){
if(!TextUtils.isEmpty(instance.getThumbnailURL())){
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
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);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
@@ -115,7 +115,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
}
boolean needDivider=false;
if(instance.contactAccount!=null){
if(instance.getContactAccount()!=null){
needDivider=true;
TextView heading=new TextView(getActivity());
heading.setTextAppearance(R.style.m3_title_small);
@@ -128,7 +128,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
hlp.leftMargin=hlp.rightMargin=V.dp(16);
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);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
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);
}
}
if(!TextUtils.isEmpty(instance.email)){
if(!TextUtils.isEmpty(instance.getContactEmail())){
needDivider=true;
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->{});
@@ -208,7 +208,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
public void onRefresh(){}
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);
try{
startActivity(intent);

View File

@@ -1,7 +1,5 @@
package org.joinmastodon.android.model;
import android.text.Html;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.catalog.CatalogInstance;
@@ -10,15 +8,8 @@ import org.parceler.Parcel;
import java.net.IDN;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@Parcel
public class Instance extends BaseModel{
/**
* The domain name of the instance.
*/
@RequiredField
public String uri;
public abstract class Instance extends BaseModel{
/**
* The title of the website.
*/
@@ -29,16 +20,6 @@ public class Instance extends BaseModel{
*/
@RequiredField
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.
*/
@@ -49,32 +30,7 @@ public class Instance extends BaseModel{
*/
// @RequiredField
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 Configuration configuration;
@@ -85,51 +41,38 @@ public class Instance extends BaseModel{
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
if(contactAccount!=null)
contactAccount.postprocess();
if(rules==null)
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(){
CatalogInstance ci=new CatalogInstance();
ci.domain=uri;
ci.normalizedDomain=IDN.toUnicode(uri);
ci.description=Html.fromHtml(shortDescription).toString().trim();
if(languages!=null&&languages.size()>0){
ci.domain=getDomain();
ci.normalizedDomain=IDN.toUnicode(getDomain());
ci.description=description.trim();
if(languages!=null && !languages.isEmpty()){
ci.language=languages.get(0);
ci.languages=languages;
}else{
ci.languages=Collections.emptyList();
ci.languages=List.of();
ci.language="unknown";
}
ci.proxiedThumbnail=thumbnail;
if(stats!=null)
ci.totalUsers=stats.userCount;
ci.proxiedThumbnail=getThumbnailURL();
// if(stats!=null)
// ci.totalUsers=stats.userCount;
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
public static class Rule{
public String id;
@@ -138,13 +81,6 @@ public class Instance extends BaseModel{
public transient CharSequence parsedText;
}
@Parcel
public static class Stats{
public int userCount;
public int statusCount;
public int domainCount;
}
@Parcel
public static class Configuration{
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;
}
}