Onboarding & signup

This commit is contained in:
Grishka
2022-03-10 18:48:24 +03:00
parent 86892e4103
commit 03c0b183cb
80 changed files with 2024 additions and 261 deletions

View File

@@ -1,11 +1,14 @@
package org.joinmastodon.android;
import android.app.Application;
import android.app.Fragment;
import android.os.Bundle;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import java.lang.reflect.InvocationTargetException;
@@ -22,9 +25,10 @@ public class MainActivity extends FragmentStackActivity{
showFragmentClearingBackStack(new SplashFragment());
}else{
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
Bundle args=new Bundle();
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
HomeFragment fragment=new HomeFragment();
args.putString("account", session.getID());
Fragment fragment=session.activated ? new HomeFragment() : new AccountActivationFragment();
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
}

View File

@@ -54,7 +54,7 @@ public class OAuthActivity extends Activity{
progress.setMessage(getString(R.string.finishing_auth));
progress.setCancelable(false);
progress.show();
new GetOauthToken(app.clientId, app.clientSecret, code)
new GetOauthToken(app.clientId, app.clientSecret, code, GetOauthToken.GrantType.AUTHORIZATION_CODE)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token token){
@@ -62,7 +62,7 @@ public class OAuthActivity extends Activity{
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account account){
AccountSessionManager.getInstance().addAccount(instance, token, account, app);
AccountSessionManager.getInstance().addAccount(instance, token, account, app, true);
progress.dismiss();
finish();
// not calling restartMainActivity() here on purpose to have it recreated (notice different flags)

View File

@@ -8,6 +8,7 @@ import android.util.Log;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
@@ -119,10 +120,19 @@ public class MastodonAPIController{
if(response.isSuccessful()){
T respObj;
try{
if(req.respTypeToken!=null)
respObj=gson.fromJson(reader, req.respTypeToken.getType());
else
respObj=gson.fromJson(reader, req.respClass);
if(BuildConfig.DEBUG){
JsonElement respJson=JsonParser.parseReader(reader);
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
if(req.respTypeToken!=null)
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
else
respObj=gson.fromJson(respJson, req.respClass);
}else{
if(req.respTypeToken!=null)
respObj=gson.fromJson(reader, req.respTypeToken.getType());
else
respObj=gson.fromJson(reader, req.respClass);
}
}catch(JsonIOException|JsonSyntaxException x){
if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
@@ -146,6 +156,7 @@ public class MastodonAPIController{
}else{
try{
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
req.onError(error.get("error").getAsString());
}catch(JsonIOException|JsonSyntaxException x){
req.onError(response.code()+" "+response.message());

View File

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

View File

@@ -0,0 +1,19 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
public class ResendConfirmationEmail extends MastodonAPIRequest<Object>{
public ResendConfirmationEmail(String email){
super(HttpMethod.POST, "/emails/confirmations", Object.class);
// setRequestBody(new Body(email));
setRequestBody(new Object());
}
private static class Body{
public String email;
public Body(String email){
this.email=email;
}
}
}

View File

@@ -8,6 +8,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.util.List;
import okhttp3.MultipartBody;
@@ -16,6 +17,7 @@ import okhttp3.RequestBody;
public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
private String displayName, bio;
private Uri avatar, cover;
private File avatarFile, coverFile;
private List<AccountField> fields;
public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List<AccountField> fields){
@@ -27,6 +29,15 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
this.fields=fields;
}
public UpdateAccountCredentials(String displayName, String bio, File avatar, File cover, List<AccountField> fields){
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
this.displayName=displayName;
this.bio=bio;
this.avatarFile=avatar;
this.coverFile=cover;
this.fields=fields;
}
@Override
public RequestBody getRequestBody(){
MultipartBody.Builder bldr=new MultipartBody.Builder()
@@ -36,9 +47,13 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
if(avatar!=null){
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null));
}else if(avatarFile!=null){
bldr.addFormDataPart("avatar", avatarFile.getName(), RequestBody.create(UiUtils.getFileMediaType(avatarFile), avatarFile));
}
if(cover!=null){
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null));
}else if(coverFile!=null){
bldr.addFormDataPart("header", coverFile.getName(), RequestBody.create(UiUtils.getFileMediaType(coverFile), coverFile));
}
if(fields.isEmpty()){
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");

View File

@@ -1,13 +1,15 @@
package org.joinmastodon.android.api.requests.oauth;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Token;
public class GetOauthToken extends MastodonAPIRequest<Token>{
public GetOauthToken(String clientID, String clientSecret, String code){
public GetOauthToken(String clientID, String clientSecret, String code, GrantType grantType){
super(HttpMethod.POST, "/oauth/token", Token.class);
setRequestBody(new Request(clientID, clientSecret, code));
setRequestBody(new Request(clientID, clientSecret, code, grantType));
}
@Override
@@ -16,17 +18,25 @@ public class GetOauthToken extends MastodonAPIRequest<Token>{
}
private static class Request{
public String grantType="authorization_code";
public GrantType grantType;
public String clientId;
public String clientSecret;
public String redirectUri=AccountSessionManager.REDIRECT_URI;
public String scope=AccountSessionManager.SCOPE;
public String code;
public Request(String clientId, String clientSecret, String code){
public Request(String clientId, String clientSecret, String code, GrantType grantType){
this.clientId=clientId;
this.clientSecret=clientSecret;
this.code=code;
this.grantType=grantType;
}
}
public enum GrantType{
@SerializedName("authorization_code")
AUTHORIZATION_CODE,
@SerializedName("client_credentials")
CLIENT_CREDENTIALS
}
}

View File

@@ -16,16 +16,18 @@ public class AccountSession{
public long infoLastUpdated;
public long instanceLastUpdated;
public Instance instance;
public boolean activated=true;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit, Instance instance){
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit, Instance instance, boolean activated){
this.token=token;
this.self=self;
this.domain=domain;
this.app=app;
this.tootCharLimit=tootCharLimit;
this.instance=instance;
this.activated=activated;
instanceLastUpdated=infoLastUpdated=System.currentTimeMillis();
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.SharedPreferences;
@@ -36,10 +37,12 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.browser.customtabs.CustomTabsIntent;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -84,8 +87,8 @@ public class AccountSessionManager{
MastodonAPIController.runInBackground(()->readCustomEmojis(domains));
}
public void addAccount(Instance instance, Token token, Account self, Application app){
AccountSession session=new AccountSession(token, self, app, instance.uri, instance.maxTootChars, instance);
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
AccountSession session=new AccountSession(token, self, app, instance.uri, instance.maxTootChars, instance, active);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();
@@ -159,18 +162,13 @@ public class AccountSessionManager{
return unauthenticatedApiController;
}
public void authenticate(Context context, Instance instance){
public void authenticate(Activity activity, Instance instance){
authenticatingInstance=instance;
ProgressDialog progress=new ProgressDialog(context);
progress.setMessage(context.getString(R.string.preparing_auth));
progress.setCancelable(false);
progress.show();
new CreateOAuthApp()
.setCallback(new Callback<Application>(){
.setCallback(new Callback<>(){
@Override
public void onSuccess(Application result){
authenticatingApp=result;
progress.dismiss();
Uri uri=new Uri.Builder()
.scheme("https")
.authority(instance.uri)
@@ -184,15 +182,15 @@ public class AccountSessionManager{
new CustomTabsIntent.Builder()
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
.build()
.launchUrl(context, uri);
.launchUrl(activity, uri);
}
@Override
public void onError(ErrorResponse error){
error.showToast(context);
progress.dismiss();
error.showToast(activity);
}
})
.wrapProgress(activity, R.string.preparing_auth, false)
.execNoAuth(instance.uri);
}

View File

@@ -17,6 +17,7 @@ import android.widget.LinearLayout;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.views.TabBar;
import org.parceler.Parcels;

View File

@@ -38,9 +38,9 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Relationship;

View File

@@ -1,5 +1,5 @@
package org.joinmastodon.android.fragments;
/*package*/ interface ScrollableToTop{
public interface ScrollableToTop{
void scrollToTop();
}

View File

@@ -1,30 +1,66 @@
package org.joinmastodon.android.fragments;
import android.app.Fragment;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogFragment;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import androidx.annotation.Nullable;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.utils.V;
public class SplashFragment extends AppKitFragment{
private View contentView;
private SizeListenerFrameLayout contentView;
private View artContainer, blueFill, greenFill;
private InterpolatingMotionEffect motionEffect;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
contentView= inflater.inflate(R.layout.fragment_splash, container, false);
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
artContainer=contentView.findViewById(R.id.art_container);
blueFill=contentView.findViewById(R.id.blue_fill);
greenFill=contentView.findViewById(R.id.green_fill);
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_clouds), V.dp(-5), V.dp(5), V.dp(-5), V.dp(5)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_right_hill), V.dp(-15), V.dp(25), V.dp(-10), V.dp(10)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_left_hill), V.dp(-25), V.dp(15), V.dp(-15), V.dp(15)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_center_hill), V.dp(-14), V.dp(14), V.dp(-5), V.dp(25)));
motionEffect.addViewEffect(new InterpolatingMotionEffect.ViewEffect(contentView.findViewById(R.id.art_plane_elephant), V.dp(-20), V.dp(12), V.dp(-20), V.dp(12)));
contentView.setSizeListener(new SizeListenerFrameLayout.OnSizeChangedListener(){
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh){
contentView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
contentView.getViewTreeObserver().removeOnPreDrawListener(this);
updateArtSize(w, h);
return true;
}
});
}
});
return contentView;
}
@@ -33,10 +69,46 @@ public class SplashFragment extends AppKitFragment{
extras.putBoolean("signup", v.getId()==R.id.btn_get_started);
Nav.go(getActivity(), InstanceCatalogFragment.class, extras);
}
//
// @Override
// public void onApplyWindowInsets(WindowInsets insets){
// if(contentView!=null)
// contentView.dispatchApplyWindowInsets(insets);
// }
private void updateArtSize(int w, int h){
float scale=w/(float)V.dp(412);
artContainer.setScaleX(scale);
artContainer.setScaleY(scale);
blueFill.setScaleY(h/2f);
greenFill.setScaleY(h-artContainer.getBottom()+V.dp(90));
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
int bottomInset=insets.getSystemWindowInsetBottom();
if(bottomInset>0 && bottomInset<V.dp(36)){
contentView.setPadding(contentView.getPaddingLeft(), contentView.getPaddingTop(), contentView.getPaddingRight(), V.dp(36));
}
((ViewGroup.MarginLayoutParams)blueFill.getLayoutParams()).topMargin=-contentView.getPaddingTop();
((ViewGroup.MarginLayoutParams)greenFill.getLayoutParams()).bottomMargin=-contentView.getPaddingBottom();
}
@Override
public boolean wantsLightStatusBar(){
return true;
}
@Override
public boolean wantsLightNavigationBar(){
return true;
}
@Override
protected void onShown(){
super.onShown();
motionEffect.activate();
}
@Override
protected void onHidden(){
super.onHidden();
motionEffect.deactivate();
}
}

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
@@ -15,6 +15,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.Relationship;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.os.Build;
@@ -10,6 +10,7 @@ import android.widget.FrameLayout;
import android.widget.LinearLayout;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.discover;
import android.graphics.drawable.Drawable;
import android.os.Bundle;

View File

@@ -1,10 +1,11 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.discover;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Status;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.discover;
import android.graphics.Canvas;
import android.graphics.Paint;

View File

@@ -0,0 +1,172 @@
package org.joinmastodon.android.fragments.onboarding;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.Toast;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.ResendConfirmationEmail;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.util.Collections;
import androidx.annotation.Nullable;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends AppKitFragment{
private String accountID;
private Button btn, backBtn;
private View buttonBar;
private Handler uiHandler=new Handler(Looper.getMainLooper());
private Runnable pollRunnable=this::tryGetAccount;
private APIRequest currentRequest;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_activation, container, false);
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->onBackButtonClick());
return view;
}
@Override
public boolean wantsLightStatusBar(){
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)!=Configuration.UI_MODE_NIGHT_YES;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
@Override
protected void onShown(){
super.onShown();
tryGetAccount();
}
@Override
protected void onHidden(){
super.onHidden();
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}else{
uiHandler.removeCallbacks(pollRunnable);
}
}
private void onButtonClick(){
startActivity(Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_EMAIL).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}
private void onBackButtonClick(){
new ResendConfirmationEmail(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
Toast.makeText(getActivity(), R.string.resent_email, Toast.LENGTH_SHORT).show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
private void tryGetAccount(){
currentRequest=new GetOwnAccount()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
currentRequest=null;
AccountSessionManager mgr=AccountSessionManager.getInstance();
AccountSession session=mgr.getAccount(accountID);
mgr.removeAccount(accountID);
mgr.addAccount(session.instance, session.token, result, session.app, true);
String newID=mgr.getLastActiveAccountID();
Bundle args=new Bundle();
args.putString("account", newID);
if(session.self.avatar!=null || session.self.displayName!=null){
File avaFile=session.self.avatar!=null ? new File(session.self.avatar) : null;
new UpdateAccountCredentials(session.self.displayName, "", avaFile, null, Collections.emptyList())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
if(avaFile!=null)
avaFile.delete();
mgr.updateAccountInfo(newID, result);
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
if(avaFile!=null)
avaFile.delete();
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
})
.exec(newID);
}else{
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
uiHandler.postDelayed(pollRunnable, 10_000L);
}
})
.exec(accountID);
}
}

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Build;
@@ -12,11 +11,12 @@ import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
@@ -28,6 +28,12 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.net.IDN;
import java.util.ArrayList;
@@ -40,14 +46,15 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
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.UsableRecyclerView;
public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
@@ -59,7 +66,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
private Button nextButton;
private MastodonAPIRequest<?> getCategoriesRequest;
private EditText searchEdit;
private UsableRecyclerView categoriesList;
private TabLayout categoriesList;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
private String currentSearchQuery;
private String currentCategory="all";
@@ -68,6 +75,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
private GetInstance loadingInstanceRequest;
private HashMap<String, Instance> instancesCache=new HashMap<>();
private ProgressDialog instanceProgressDialog;
private View buttonBar;
private boolean isSignup;
@@ -145,8 +153,14 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
CatalogCategory all=new CatalogCategory();
all.category="all";
categories.add(all);
categories.addAll(result);
categoriesList.getAdapter().notifyItemRangeInserted(0, categories.size());
result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add);
for(CatalogCategory cat:categories){
int titleRes=getTitleForCategory(cat.category);
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
emoji.setImageResource(getEmojiForCategory(cat.category));
categoriesList.addTab(tab);
}
}
@Override
@@ -170,6 +184,24 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
searchEdit=headerView.findViewById(R.id.search_edit);
categoriesList=headerView.findViewById(R.id.categories_list);
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
CatalogCategory category=categories.get(tab.getPosition());
currentCategory=category.category;
updateFilteredList();
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
}
});
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
@@ -187,8 +219,6 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
public void afterTextChanged(Editable s){
}
});
categoriesList.setAdapter(new CategoriesAdapter());
categoriesList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
@@ -201,6 +231,13 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
super.onViewCreated(view, savedInstanceState);
nextButton=view.findViewById(R.id.btn_next);
nextButton.setOnClickListener(this::onNextClick);
nextButton.setEnabled(chosenInstance!=null);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
buttonBar=view.findViewById(R.id.button_bar);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
private void onNextClick(View v){
@@ -218,12 +255,71 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
private void proceedWithAuthOrSignup(Instance instance){
if(isSignup){
Toast.makeText(getActivity(), "not implemented yet", Toast.LENGTH_SHORT).show();
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}else{
AccountSessionManager.getInstance().authenticate(getActivity(), instance);
}
}
// private String getEmojiForCategory(String category){
// return switch(category){
// case "all" -> "💬";
// case "academia" -> "📚";
// case "activism" -> "✊";
// case "food" -> "🍕";
// case "furry" -> "🦁";
// case "games" -> "🕹";
// case "general" -> "🐘";
// case "journalism" -> "📰";
// case "lgbt" -> "🏳️‍🌈";
// case "regional" -> "📍";
// case "art" -> "🎨";
// case "music" -> "🎼";
// case "tech" -> "📱";
// default -> "❓";
// };
// }
private int getEmojiForCategory(String category){
return switch(category){
case "all" -> R.drawable.ic_category_all;
case "academia" -> R.drawable.ic_category_academia;
case "activism" -> R.drawable.ic_category_activism;
case "food" -> R.drawable.ic_category_food;
case "furry" -> R.drawable.ic_category_furry;
case "games" -> R.drawable.ic_category_games;
case "general" -> R.drawable.ic_category_general;
case "journalism" -> R.drawable.ic_category_journalism;
case "lgbt" -> R.drawable.ic_category_lgbt;
case "regional" -> R.drawable.ic_category_regional;
case "art" -> R.drawable.ic_category_art;
case "music" -> R.drawable.ic_category_music;
case "tech" -> R.drawable.ic_category_tech;
default -> R.drawable.ic_category_unknown;
};
}
private int getTitleForCategory(String category){
return switch(category){
case "all" -> R.string.category_all;
case "academia" -> R.string.category_academia;
case "activism" -> R.string.category_activism;
case "food" -> R.string.category_food;
case "furry" -> R.string.category_furry;
case "games" -> R.string.category_games;
case "general" -> R.string.category_general;
case "journalism" -> R.string.category_journalism;
case "lgbt" -> R.string.category_lgbt;
case "regional" -> R.string.category_regional;
case "art" -> R.string.category_art;
case "music" -> R.string.category_music;
case "tech" -> R.string.category_tech;
default -> 0;
};
}
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
@@ -290,6 +386,8 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
}
private void loadInstanceInfo(String _domain){
if(TextUtils.isEmpty(_domain))
return;
String domain;
try{
domain=IDN.toASCII(_domain);
@@ -299,7 +397,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
Instance cachedInstance=instancesCache.get(domain);
if(cachedInstance!=null){
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(currentSearchQuery))
if(ci.domain.equals(domain))
return;
}
CatalogInstance ci=cachedInstance.toCatalogInstance();
@@ -330,7 +428,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
if(domain.equals(currentSearchQuery)){
boolean found=false;
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(currentSearchQuery)){
if(ci.domain.equals(domain)){
found=true;
break;
}
@@ -350,7 +448,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
new AlertDialog.Builder(getActivity())
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(getString(R.string.not_a_mastodon_instance, domain)+"\n\n"+((MastodonErrorResponse)error).error)
.setPositiveButton(R.string.ok, null)
@@ -360,6 +458,17 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
}).execNoAuth(domain);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceViewHolder>{
public InstancesAdapter(){
super(imgLoader);
@@ -399,13 +508,17 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
userCount=findViewById(R.id.user_count);
lang=findViewById(R.id.lang);
radioButton=findViewById(R.id.radiobtn);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
}
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
userCount.setText(""+item.totalUsers);
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
radioButton.setChecked(chosenInstance==item);
}
@@ -430,57 +543,4 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
loadInstanceInfo(chosenInstance.domain);
}
}
private class CategoriesAdapter extends RecyclerView.Adapter<CategoryViewHolder>{
@NonNull
@Override
public CategoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new CategoryViewHolder();
}
@Override
public void onBindViewHolder(@NonNull CategoryViewHolder holder, int position){
holder.bind(categories.get(position));
}
@Override
public int getItemCount(){
return categories.size();
}
}
private class CategoryViewHolder extends BindableViewHolder<CatalogCategory> implements UsableRecyclerView.Clickable{
private final RadioButton radioButton;
public CategoryViewHolder(){
super(getActivity(), R.layout.item_instance_category, categoriesList);
radioButton=findViewById(R.id.radiobtn);
}
@Override
public void onBind(CatalogCategory item){
radioButton.setText(item.category);
radioButton.setChecked(item.category.equals(currentCategory));
}
@Override
public void onClick(){
if(currentCategory.equals(item.category))
return;
int i=0;
for(CatalogCategory c:categories){
if(c.category.equals(currentCategory)){
RecyclerView.ViewHolder holder=categoriesList.findViewHolderForAdapterPosition(i);
if(holder!=null){
((CategoryViewHolder)holder).radioButton.setChecked(false);
}
break;
}
i++;
}
currentCategory=item.category;
radioButton.setChecked(true);
updateFilteredList();
}
}
}

View File

@@ -0,0 +1,137 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
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.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
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.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.UsableRecyclerView;
public class InstanceRulesFragment extends AppKitFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
private View buttonBar;
private Instance instance;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
title.setText(R.string.instance_rules_title);
subtitle.setText(getString(R.string.instance_rules_subtitle, instance.uri));
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), SignupFragment.class, args);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ItemViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
holder.bind(instance.rules.get(position));
}
@Override
public int getItemCount(){
return instance.rules.size();
}
}
private class ItemViewHolder extends BindableViewHolder<Instance.Rule>{
private final TextView title, subtitle;
private final ImageView checkbox;
public ItemViewHolder(){
super(getActivity(), R.layout.item_report_choice, list);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
checkbox=findViewById(R.id.checkbox);
subtitle.setVisibility(View.GONE);
}
@Override
public void onBind(Instance.Rule item){
title.setText(item.text);
}
}
}

View File

@@ -0,0 +1,290 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.accounts.RegisterAccount;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.api.requests.oauth.GetOauthToken;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import androidx.annotation.Nullable;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class SignupFragment extends AppKitFragment{
private static final int AVATAR_RESULT=198;
private static final String TAG="SignupFragment";
private Instance instance;
private EditText displayName, username, email, password;
private Button btn;
private View buttonBar;
private TextWatcher buttonStateUpdater=new SimpleTextWatcher(e->updateButtonState());
private ImageView avatar;
private APIRequest currentBackgroundRequest;
private Application apiApplication;
private Token apiToken;
private boolean submitAfterGettingToken;
private ProgressDialog progressDialog;
private Uri avatarUri;
private File avatarFile;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false);
TextView title=view.findViewById(R.id.title);
TextView domain=view.findViewById(R.id.domain);
displayName=view.findViewById(R.id.display_name);
username=view.findViewById(R.id.username);
email=view.findViewById(R.id.email);
password=view.findViewById(R.id.password);
avatar=view.findViewById(R.id.avatar);
View avaWrap=view.findViewById(R.id.ava_wrap);
title.setText(getString(R.string.signup_title, instance.uri));
domain.setText('@'+instance.uri);
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
username.getViewTreeObserver().removeOnPreDrawListener(this);
username.setPadding(username.getPaddingLeft(), username.getPaddingTop(), domain.getWidth(), username.getPaddingBottom());
return true;
}
});
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
updateButtonState();
username.addTextChangedListener(buttonStateUpdater);
email.addTextChangedListener(buttonStateUpdater);
password.addTextChangedListener(buttonStateUpdater);
avaWrap.setOutlineProvider(OutlineProviders.roundedRect(22));
avaWrap.setClipToOutline(true);
avaWrap.setOnClickListener(v->onAvatarClick());
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
}
private void onButtonClick(){
showProgressDialog();
if(currentBackgroundRequest!=null){
submitAfterGettingToken=true;
}else if(apiApplication==null){
submitAfterGettingToken=true;
createAppAndGetToken();
}else if(apiToken==null){
submitAfterGettingToken=true;
getToken();
}else{
submit();
}
}
private void copyAvatar(Runnable onDone){
// Need to copy the avatar from the content provider to somewhere accessible in case the app gets killed between signup and account activation
Activity activity=getActivity();
MastodonAPIController.runInBackground(()->{
String origName=UiUtils.getFileName(avatarUri);
avatarFile=new File(activity.getCacheDir(), System.currentTimeMillis()+origName.substring(origName.lastIndexOf('.')));
try(InputStream in=activity.getContentResolver().openInputStream(avatarUri);
FileOutputStream out=new FileOutputStream(avatarFile)){
byte[] buf=new byte[10240];
int read;
while((read=in.read(buf))>0){
out.write(buf, 0, read);
}
}catch(IOException x){
Log.w(TAG, "copyAvatar: error copying", x);
}
activity.runOnUiThread(onDone);
});
}
private void submit(){
if(avatarUri!=null && (avatarFile==null || !avatarFile.exists())){
copyAvatar(this::actuallySubmit);
}else{
actuallySubmit();
}
}
private void actuallySubmit(){
String username=this.username.getText().toString();
String email=this.email.getText().toString();
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){
progressDialog.dismiss();
progressDialog=null;
Account fakeAccount=new Account();
fakeAccount.acct=fakeAccount.username=username;
fakeAccount.id="tmp"+System.currentTimeMillis();
fakeAccount.displayName=displayName.getText().toString();
if(avatarFile!=null)
fakeAccount.avatar=avatarFile.getAbsolutePath();
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, false);
Bundle args=new Bundle();
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
progressDialog.dismiss();
progressDialog=null;
}
})
.exec(instance.uri, apiToken);
}
private void showProgressDialog(){
progressDialog=new ProgressDialog(getActivity());
progressDialog.setMessage(getString(R.string.loading));
progressDialog.setCancelable(false);
progressDialog.show();
}
private void updateButtonState(){
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8);
}
private void createAppAndGetToken(){
currentBackgroundRequest=new CreateOAuthApp()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Application result){
apiApplication=result;
getToken();
}
@Override
public void onError(ErrorResponse error){
currentBackgroundRequest=null;
if(submitAfterGettingToken){
submitAfterGettingToken=false;
progressDialog.dismiss();
progressDialog=null;
error.showToast(getActivity());
}
}
})
.execNoAuth(instance.uri);
}
private void getToken(){
currentBackgroundRequest=new GetOauthToken(apiApplication.clientId, apiApplication.clientSecret, null, GetOauthToken.GrantType.CLIENT_CREDENTIALS)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){
currentBackgroundRequest=null;
apiToken=result;
if(submitAfterGettingToken){
submitAfterGettingToken=false;
submit();
}
}
@Override
public void onError(ErrorResponse error){
currentBackgroundRequest=null;
if(submitAfterGettingToken){
submitAfterGettingToken=false;
progressDialog.dismiss();
progressDialog=null;
error.showToast(getActivity());
}
}
})
.execNoAuth(instance.uri);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==AVATAR_RESULT && resultCode==Activity.RESULT_OK){
avatarUri=data.getData();
if(avatarFile!=null && avatarFile.exists())
avatarFile.delete();
ViewImageLoader.load(avatar, getResources().getDrawable(R.drawable.default_avatar), new UrlImageLoaderRequest(avatarUri, V.dp(100), V.dp(100)));
}
}
private void onAvatarClick(){
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*").addCategory(Intent.CATEGORY_OPENABLE), AVATAR_RESULT);
}
}

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.os.Build;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.graphics.Canvas;
@@ -15,10 +15,10 @@ import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
@@ -33,7 +33,6 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.os.Build;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.os.Build;
@@ -8,17 +8,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.reports.SendReport;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ReportReason;
@@ -26,8 +20,6 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.report;
import android.os.Bundle;

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.fragments;
package org.joinmastodon.android.fragments.report;
import android.os.Bundle;

View File

@@ -1,18 +1,18 @@
package org.joinmastodon.android.model;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Html;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.catalog.CatalogInstance;
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.
@@ -84,6 +84,8 @@ public class Instance extends BaseModel{
super.postprocess();
if(contactAccount!=null)
contactAccount.postprocess();
if(rules==null)
rules=Collections.emptyList();
}
@Override
@@ -123,93 +125,16 @@ public class Instance extends BaseModel{
return ci;
}
public static class Rule implements Parcelable{
@Parcel
public static class Rule{
public String id;
public String text;
@Override
public int describeContents(){
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags){
dest.writeString(this.id);
dest.writeString(this.text);
}
public void readFromParcel(Parcel source){
this.id=source.readString();
this.text=source.readString();
}
public Rule(){
}
protected Rule(Parcel in){
this.id=in.readString();
this.text=in.readString();
}
public static final Parcelable.Creator<Rule> CREATOR=new Parcelable.Creator<Rule>(){
@Override
public Rule createFromParcel(Parcel source){
return new Rule(source);
}
@Override
public Rule[] newArray(int size){
return new Rule[size];
}
};
}
public static class Stats implements Parcelable{
@Parcel
public static class Stats{
public int userCount;
public int statusCount;
public int domainCount;
@Override
public int describeContents(){
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags){
dest.writeInt(this.userCount);
dest.writeInt(this.statusCount);
dest.writeInt(this.domainCount);
}
public void readFromParcel(Parcel source){
this.userCount=source.readInt();
this.statusCount=source.readInt();
this.domainCount=source.readInt();
}
public Stats(){
}
protected Stats(Parcel in){
this.userCount=in.readInt();
this.statusCount=in.readInt();
this.domainCount=in.readInt();
}
public static final Parcelable.Creator<Stats> CREATOR=new Parcelable.Creator<Stats>(){
@Override
public Stats createFromParcel(Parcel source){
return new Stats(source);
}
@Override
public Stats[] newArray(int size){
return new Stats[size];
}
};
}
}

View File

@@ -44,7 +44,8 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
View child=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(child);
if(pos<totalItems-1 && (drawDividerPredicate==null || drawDividerPredicate.test(parent.getChildViewHolder(child)))){
float y=Math.round(child.getY()+child.getHeight()-paint.getStrokeWidth()/2f);
float y=Math.round(child.getY()+child.getHeight());
y-=(y-paint.getStrokeWidth()/2f)%1f; // Make sure the line aligns with the pixel grid
paint.setAlpha(Math.round(255f*child.getAlpha()));
c.drawLine(padLeft+child.getX(), y, child.getX()+child.getWidth()-padRight, y, paint);
}

View File

@@ -0,0 +1,134 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import java.util.ArrayList;
public class InterpolatingMotionEffect implements SensorEventListener{
private SensorManager sm;
private WindowManager wm;
private float[] rollBuffer = new float[9], pitchBuffer = new float[9];
private int bufferOffset;
private Sensor accelerometer;
private boolean accelerometerEnabled;
private ArrayList<ViewEffect> views=new ArrayList<>();
public InterpolatingMotionEffect(Context context){
sm=context.getSystemService(SensorManager.class);
wm=context.getSystemService(WindowManager.class);
accelerometer=sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}
public void activate(){
if(accelerometer==null || accelerometerEnabled)
return;
sm.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
accelerometerEnabled=true;
}
public void deactivate(){
if(accelerometer==null || !accelerometerEnabled)
return;
sm.unregisterListener(this);
accelerometerEnabled=false;
}
@Override
public void onSensorChanged(SensorEvent event){
int rotation=wm.getDefaultDisplay().getRotation();
float x=event.values[0]/SensorManager.GRAVITY_EARTH;
float y=event.values[1]/SensorManager.GRAVITY_EARTH;
float z=event.values[2]/SensorManager.GRAVITY_EARTH;
float pitch=(float) (Math.atan2(x, Math.sqrt(y*y+z*z))/Math.PI*2.0);
float roll=(float) (Math.atan2(y, Math.sqrt(x*x+z*z))/Math.PI*2.0);
switch(rotation){
case Surface.ROTATION_0:
break;
case Surface.ROTATION_90:{
float tmp=pitch;
pitch=roll;
roll=tmp;
break;
}
case Surface.ROTATION_180:
roll=-roll;
pitch=-pitch;
break;
case Surface.ROTATION_270:{
float tmp=-pitch;
pitch=roll;
roll=tmp;
break;
}
}
rollBuffer[bufferOffset]=roll;
pitchBuffer[bufferOffset]=pitch;
bufferOffset=(bufferOffset+1)%rollBuffer.length;
roll=pitch=0;
for(int i=0; i<rollBuffer.length; i++){
roll+=rollBuffer[i];
pitch+=pitchBuffer[i];
}
roll/=rollBuffer.length;
pitch/=rollBuffer.length;
if(roll>1f){
roll=2f-roll;
}else if(roll<-1f){
roll=-2f-roll;
}
for(ViewEffect view:views){
view.update(pitch, roll);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy){
}
public void addViewEffect(ViewEffect effect){
views.add(effect);
}
public void removeViewEffect(ViewEffect effect){
views.remove(effect);
}
public void removeAllViewEffects(){
views.clear();
}
public static class ViewEffect{
private View view;
private float minX, maxX, minY, maxY;
public ViewEffect(View view, float minX, float maxX, float minY, float maxY){
this.view=view;
this.minX=minX;
this.maxX=maxX;
this.minY=minY;
this.maxY=maxY;
}
private void update(float x, float y){
view.setTranslationX(lerp(maxX, minX, (x+1f)/2f));
view.setTranslationY(lerp(minY, maxY, (y+1f)/2f));
}
private static float lerp(float startValue, float endValue, float fraction) {
return startValue + (fraction * (endValue - startValue));
}
}
}

View File

@@ -19,7 +19,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ReportReasonChoiceFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;

View File

@@ -13,6 +13,7 @@ import android.os.Looper;
import android.provider.OpenableColumns;
import android.text.Spanned;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.TextView;
@@ -33,6 +34,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import java.io.File;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
@@ -49,6 +51,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import okhttp3.MediaType;
public class UiUtils{
private static Handler mainHandler=new Handler(Looper.getMainLooper());
@@ -143,6 +146,11 @@ public class UiUtils{
return uri.getLastPathSegment();
}
public static MediaType getFileMediaType(File file){
String name=file.getName();
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.')+1)));
}
public static void loadCustomEmojiInTextView(TextView view){
CharSequence _text=view.getText();
if(!(_text instanceof Spanned))

View File

@@ -0,0 +1,38 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
public class SizeListenerFrameLayout extends FrameLayout{
private OnSizeChangedListener sizeListener;
public SizeListenerFrameLayout(Context context){
super(context);
}
public SizeListenerFrameLayout(Context context, @Nullable AttributeSet attrs){
super(context, attrs);
}
public SizeListenerFrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
if(sizeListener!=null)
sizeListener.onSizeChanged(w, h, oldw, oldh);
}
public void setSizeListener(OnSizeChangedListener sizeListener){
this.sizeListener=sizeListener;
}
@FunctionalInterface
public interface OnSizeChangedListener{
void onSizeChanged(int w, int h, int oldw, int oldh);
}
}

View File

@@ -2,8 +2,6 @@ package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
@@ -32,13 +30,6 @@ public class SizeListenerLinearLayout extends LinearLayout{
public void setSizeListener(OnSizeChangedListener sizeListener){
this.sizeListener=sizeListener;
}
//
// @Override
// public View findFocus(){
// View v=super.findFocus();
// Log.w("11", "findFocus() "+v);
// return v;
// }
@FunctionalInterface
public interface OnSizeChangedListener{