Compare commits

...

53 Commits

Author SHA1 Message Date
Grishka
dd582c4bee Update locales & bump version 2023-02-14 03:57:49 +03:00
Grishka
3a0d314af0 Merge branch 'l10n_master' 2023-02-14 03:49:36 +03:00
Grishka
634408b8cb Minor onboarding tweaks 2023-02-12 14:25:03 +03:00
Grishka
f050e3f22d Fix #500 2023-02-12 04:03:09 +03:00
Grishka
8e9531b718 Fix #528 2023-02-12 03:58:24 +03:00
Grishka
64fbbb2f07 Minor onboarding stuff 2023-02-10 21:09:06 +03:00
Gregory K
5c2f72a706 Merge pull request #521 from FineFindus/fix/typos
fix: typos
2023-01-30 01:54:30 +03:00
Grishka
b153a64373 Signup flow redesign WIP 2023-01-30 01:54:13 +03:00
FineFindus
1124486f1f fix(Instance): typo langauges => languages 2023-01-26 20:56:15 +01:00
Grishka
bcb3e217cd More onboarding updates 2023-01-26 01:38:29 +03:00
Grishka
a1798b6666 Update onboarding 2023-01-24 23:43:56 +03:00
Gregory K
58ab0c0fc1 Merge pull request #516 from sk22/improve-compose-toolbar-hitbox
Bigger hitbox for items in compose toolbar
2023-01-23 17:17:20 +03:00
sk
32a8d38edf bigger hitbox for items in compose toolbar 2023-01-23 14:54:39 +01:00
Gregory K
a5c753a9f8 Merge pull request #515 from sk22/allow-notifications-toolbar-tab
Enable scrolling to top by tapping Notifications toolbar
2023-01-23 15:38:07 +03:00
sk
66cede567e enable scrolling to top via toolbar 2023-01-23 12:49:55 +01:00
Grishka
c67b2b35f3 Fix #509 2023-01-22 02:08:55 +03:00
Grishka
8588ca8ae3 Fix #510 2023-01-22 02:05:36 +03:00
Grishka
7bb280e8b8 Fix #69 (nice) 2023-01-22 02:04:12 +03:00
Grishka
ddfeaabd44 Fix #512 2023-01-22 01:12:39 +03:00
Gregory K
51219bf98a Merge pull request #513 from sk22/fix-has-spoiler-restore
Fix wrong "hasSpoiler" value on restore
2023-01-22 01:11:42 +03:00
sk
512cb70347 fix wrong "hasSpoiler" value on restore
closes sk22#324
2023-01-21 23:06:31 +01:00
Grishka
6e718d6765 Save last seen home timeline post via markers API 2023-01-18 20:29:49 +03:00
Grishka
b26d491eda remove log 2023-01-18 20:10:03 +03:00
Grishka
abdbab9d7b Allow viewing alt text on images
closes #100
2023-01-18 20:09:27 +03:00
Grishka
af1c7194e6 Workaround to fix #497 2023-01-18 18:41:48 +03:00
Gregory K
ab3a98fd60 Merge pull request #439 from Poussinou/patch-1
Update README.md
2023-01-15 22:15:30 +03:00
Grishka
6e6fdbccd5 Fix #484 2023-01-15 11:40:06 +03:00
Grishka
1764e5f3d1 Paginate trending posts 2023-01-15 11:09:53 +03:00
Grishka
836c493951 Fix #499 2023-01-12 00:40:50 +03:00
Grishka
d667b8fa98 Fix #498 2023-01-11 13:06:40 +03:00
Gregory K
6a1032cd61 Merge pull request #492 from tylersaunders/mastodon-photopicker
Update ComposeFragment to use the photopicker.
2023-01-11 12:49:19 +03:00
Tyler Saunders
557d535e5a Update ComposeFragment to use the photopicker.
The android platform has a great photopicker, and this change checks
for the current device's sdk version, and uses the photopicker if it's
available on the device.

For pre Android R sdkrev2 devices, the experience remains unchanged.
2023-01-03 17:21:22 +00:00
Gregory K
60517b00f3 Merge pull request #491 from mishnz/master
Second fix for MIME64 inconsistency in serverKey.
2023-01-02 14:23:23 +03:00
mishnz
faf5e8e82b Merge branch 'master' of https://github.com/mishnz/mastodon-android 2023-01-03 00:16:12 +13:00
mishnz
7264982761 serverKey assignment was missing, corrected. 2023-01-03 00:15:45 +13:00
mishnz
fedf74258f Merge branch 'mastodon:master' into master 2023-01-03 00:06:53 +13:00
mishnz
def4960be6 Second fix for MIME64 inconsistency in serverKey.
The previous fix https://github.com/mastodon/mastodon-android/pull/486 would break any connections to any instances using WEB_SAFE MIME64 encoding on the serverKey, which actually appears to be the usual case.
This update reverts to the previous logic, but also converts standard MIME64 characters ('/' and '+') to their WEB_SAFE equivalents.
This ensures the standard case of WEB_SAFE BASE64 serverKeys and the anomolous case of DEFAULT BASE64 keys both work.
2023-01-02 23:56:52 +13:00
Eugen Rochko
525cc69c70 Merge pull request #486 from mishnz/master
The Mastodon server does not currently use URL_SAFE encoding on its s…
2023-01-01 10:01:17 +01:00
mishnz
7ed1b164b5 The Mastodon server does not currently use URL_SAFE encoding on its serverKey. Using URL_SAFE in this client means the client will crash for any server that uses a key that generates a Mime64 string containing a "+" or "/". This change removes the URL_SAFE logic. See: https://github.com/mastodon/mastodon-android/issues/483 2023-01-01 18:50:09 +13:00
Gregory K
a5fa44213d Merge pull request #474 from sk22/fix-wrong-visibility-on-edit
Only load default status visibility if not editing
2022-12-21 11:25:14 +03:00
sk
68e9d9d91c only load default visibility if not editing
fix mastodon#306
2022-12-21 09:24:09 +01:00
Grishka
d9df150cf8 Fix #472 2022-12-20 12:14:56 +03:00
Grishka
6f2e5a63d7 This is officially the first Xiaomi workaround in this app 🎉
#469
2022-12-19 20:27:56 +03:00
Grishka
e0febda372 Minor fixes 2022-12-15 20:16:32 +03:00
Grishka
069d141451 Fix crash in ComposeFragment::onSaveInstanceState in release builds
fixes #452, fixes #453, fixes #457
2022-12-15 20:02:26 +03:00
Grishka
166401ea18 Signup flow redesign 2022-12-15 19:41:38 +03:00
Gregory K
b79ba71228 Merge pull request #445 from sk22/better-inline-emoji-search
Improve inline emoji search
2022-12-08 23:22:24 +03:00
Grishka
2903874dbc Splash fragment redesign 2022-12-08 23:22:10 +03:00
Poussinou
0dcdda75be fix typo in README 2022-12-08 12:45:28 +09:30
Grishka
202a5f9581 Fix #443 again 2022-12-08 01:48:23 +03:00
sk
622c6d503d improve inline emoji search
closes #131
closes mastodon#219
2022-12-07 16:34:52 +01:00
Grishka
e10faeefc4 Update languages 2022-12-06 19:18:51 +03:00
Poussinou
e9fe4a82df Update README.md 2022-12-04 05:26:19 +09:30
99 changed files with 3782 additions and 810 deletions

View File

@@ -3,10 +3,17 @@ Mastodon for Android
[![Crowdin](https://badges.crowdin.net/mastodon-for-android/localized.svg)](https://crowdin.com/project/mastodon-for-android)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
This is the repository for the official Android app for Mastodon.
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/org.joinmastodon.android/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=org.joinmastodon.android)
Or get the APK from the [The Releases Section](https://github.com/mastodon/mastodon-android/releases/latest).
## Contributing
Our goal is delivering a polished, professionally designed and user-friendly app. We proceed according to wireframes provided by a professional UX designer that works with Mastodon gGmbH. This means that any outside contributions that change the app visually must first be coordinated with the UX designer. *This can take time.* Furthermore, we work off of an internal roadmap and aim for feature-parity and consistency with our iOS app. The iOS app is designated as the "primary" between the two, therefore, if you want to request features, please do so in the [Mastodon for iOS](https://github.com/mastodon/mastodon-ios) repository, as you are requesting a feature to be both in iOS and Android (exceptions being system integrations specific to Android). On the other hand, any contributions that improve existing functionality, performance, or accessibility should not have any roadblocks to being merged.

View File

@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
android.enableJetifier=false

View File

@@ -9,13 +9,10 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 45
versionName "1.1.4"
versionCode 50
versionName "1.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
"ja-rJP", "kab", "ko-rKR", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ru-rRU",
"sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -91,4 +88,4 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
}
}

View File

@@ -44,6 +44,10 @@
*;
}
-keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; }
-keep class **$$Parcelable { *; }
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
-keepattributes LineNumberTable

View File

@@ -11,6 +11,13 @@
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
<application
android:name=".MastodonApp"
android:allowBackup="true"

View File

@@ -64,7 +64,7 @@ public class OAuthActivity extends Activity{
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account account){
AccountSessionManager.getInstance().addAccount(instance, token, account, app, true);
AccountSessionManager.getInstance().addAccount(instance, token, account, app, null);
progress.dismiss();
finish();
// not calling restartMainActivity() here on purpose to have it recreated (notice different flags)

View File

@@ -162,6 +162,8 @@ public class PushSubscriptionManager{
@Override
public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{
result.serverKey=result.serverKey.replace('/','_');
result.serverKey=result.serverKey.replace('+','-');
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);

View File

@@ -9,7 +9,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
Request r=new Request();
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
r.data.alerts=alerts;
r.data.policy=policy;
r.policy=policy;
r.subscription.keys.p256dh=encryptionKey;
r.subscription.keys.auth=authKey;
setRequestBody(r);
@@ -18,6 +18,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Request{
public Subscription subscription=new Subscription();
public Data data=new Data();
public PushSubscription.Policy policy;
private static class Keys{
public String p256dh;
@@ -31,7 +32,6 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Data{
public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
}
}
}

View File

@@ -3,23 +3,36 @@ package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription;
import java.io.IOException;
import okhttp3.Response;
public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{
private final PushSubscription.Policy policy;
public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
super(HttpMethod.PUT, "/push/subscription", PushSubscription.class);
setRequestBody(new Request(alerts, policy));
this.policy=policy;
}
@Override
public void validateAndPostprocessResponse(PushSubscription respObj, Response httpResponse) throws IOException{
super.validateAndPostprocessResponse(respObj, httpResponse);
respObj.policy=policy;
}
private static class Request{
public Data data=new Data();
public PushSubscription.Policy policy;
public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
this.data.alerts=alerts;
this.data.policy=policy;
this.policy=policy;
}
private static class Data{
public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
}
}
}

View File

@@ -8,9 +8,11 @@ import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetTrendingStatuses extends MastodonAPIRequest<List<Status>>{
public GetTrendingStatuses(int limit){
public GetTrendingStatuses(int offset, int limit){
super(HttpMethod.GET, "/trends/statuses", new TypeToken<>(){});
if(limit>0)
addQueryParameter("limit", ""+limit);
if(offset>0)
addQueryParameter("offset", ""+offset);
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.session;
public class AccountActivationInfo{
public String email;
public long lastEmailConfirmationResend;
public AccountActivationInfo(String email, long lastEmailConfirmationResend){
this.email=email;
this.lastEmailConfirmationResend=lastEmailConfirmationResend;
}
}

View File

@@ -28,17 +28,19 @@ public class AccountSession{
public long filtersLastUpdated;
public List<Filter> wordFilters=new ArrayList<>();
public String pushAccountID;
public AccountActivationInfo activationInfo;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager;
AccountSession(Token token, Account self, Application app, String domain, boolean activated){
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token;
this.self=self;
this.domain=domain;
this.app=app;
this.activated=activated;
this.activationInfo=activationInfo;
infoLastUpdated=System.currentTimeMillis();
}

View File

@@ -100,9 +100,9 @@ public class AccountSessionManager{
maybeUpdateShortcuts();
}
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
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, active);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();

View File

@@ -1,5 +1,7 @@
package org.joinmastodon.android.fragments;
import static android.os.ext.SdkExtensions.getExtensionVersion;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -20,6 +22,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.Editable;
import android.text.InputFilter;
@@ -211,7 +214,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
else
charLimit=500;
loadDefaultStatusVisibility(savedInstanceState);
if (editingStatus == null) loadDefaultStatusVisibility(savedInstanceState);
}
@Override
@@ -330,6 +333,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable());
spoilerEdit.setBackground(spoilerBg);
if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){
hasSpoiler=true;
spoilerEdit.setVisibility(View.VISIBLE);
spoilerBtn.setSelected(true);
}else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){
@@ -410,6 +414,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mainEditText.setSelectionListener(this);
mainEditText.addTextChangedListener(new TextWatcher(){
private int lastChangeStart, lastChangeCount;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
@@ -419,6 +425,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void onTextChanged(CharSequence s, int start, int before, int count){
if(s.length()==0)
return;
lastChangeStart=start;
lastChangeCount=count;
}
@Override
public void afterTextChanged(Editable s){
if(s.length()==0)
return;
int start=lastChangeStart;
int count=lastChangeCount;
// offset one char back to catch an already typed '@' or '#' or ':'
int realStart=start;
start=Math.max(0, start-1);
@@ -464,10 +480,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
editable.removeSpan(span);
}
}
}
@Override
public void afterTextChanged(Editable s){
updateCharCounter();
}
});
@@ -514,7 +527,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
DraftMediaAttachment da=new DraftMediaAttachment();
da.serverAttachment=att;
da.description=att.description;
da.uri=Uri.parse(att.previewUrl);
da.uri=att.previewUrl!=null ? Uri.parse(att.previewUrl) : null;
da.state=AttachmentUploadState.DONE;
attachmentsView.addView(createMediaAttachmentView(da));
attachments.add(da);
@@ -782,14 +795,38 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
.show();
}
/**
* Builds the correct intent for the device version to select media.
*
* <p>For Device version > T or R_SDK_v2, use the android platform photopicker via
* {@link MediaStore#ACTION_PICK_IMAGES}
*
* <p>For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
*/
private void openFilePicker(){
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
if(instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
intent.putExtra(Intent.EXTRA_MIME_TYPES, instance.configuration.mediaAttachments.supportedMimeTypes.toArray(new String[0]));
Intent intent;
boolean usePhotoPicker=UiUtils.isPhotoPickerAvailable();
if(usePhotoPicker){
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MAX_ATTACHMENTS-getMediaAttachmentsCount());
}else{
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
}
if(!usePhotoPicker && instance.configuration!=null &&
instance.configuration.mediaAttachments!=null &&
instance.configuration.mediaAttachments.supportedMimeTypes!=null &&
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
intent.putExtra(Intent.EXTRA_MIME_TYPES,
instance.configuration.mediaAttachments.supportedMimeTypes.toArray(
new String[0]));
}else{
if(!usePhotoPicker){
// If photo picker is being used these are the default mimetypes.
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
}
}
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(intent, MEDIA_RESULT);
@@ -871,7 +908,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
ImageView img=thumb.findViewById(R.id.thumb);
if(draft.serverAttachment!=null){
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
if(draft.serverAttachment.previewUrl!=null)
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
}else{
if(draft.mimeType.startsWith("image/")){
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));

View File

@@ -20,6 +20,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -31,6 +32,7 @@ import java.util.ArrayList;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
@@ -235,6 +237,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
new AccountSwitcherSheet(getActivity()).show();
return true;
}
if(tab==R.id.tab_home){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
}
return false;
}

View File

@@ -25,6 +25,7 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
@@ -61,6 +62,7 @@ public class HomeTimelineFragment extends StatusListFragment{
private AnimatorSet currentNewPostsAnim;
private String maxID;
private String lastSavedMarkerID;
public HomeTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@@ -142,6 +144,29 @@ public class HomeTimelineFragment extends StatusListFragment{
}
}
@Override
protected void onHidden(){
super.onHidden();
if(!data.isEmpty()){
String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID;
if(!topPostID.equals(lastSavedMarkerID)){
lastSavedMarkerID=topPostID;
new SaveMarkers(topPostID, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SaveMarkers.Response result){
}
@Override
public void onError(ErrorResponse error){
lastSavedMarkerID=null;
}
})
.exec(accountID);
}
}
}
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status), true);
}

View File

@@ -72,6 +72,18 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
scrollToTop();
}
});
pager.setOffscreenPageLimit(4);
pager.setAdapter(new DiscoverPagerAdapter());
@@ -137,6 +149,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
protected void updateToolbar(){
super.updateToolbar();
getToolbar().setOutlineProvider(null);
getToolbar().setOnClickListener(v->scrollToTop());
}
private NotificationsListFragment getFragmentForPage(int page){

View File

@@ -281,7 +281,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
}
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
}
return true;

View File

@@ -37,9 +37,11 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
@@ -55,6 +57,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
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.imageloader.ImageCache;
@@ -122,6 +125,19 @@ public class SettingsFragment extends MastodonToolbarFragment{
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
if(BuildConfig.DEBUG){
items.add(new RedHeaderItem("Debug options"));
items.add(new TextItem("Test e-mail confirmation flow", ()->{
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false;
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("debug", true);
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
}));
}
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
@@ -346,6 +362,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.text=getString(text);
}
public HeaderItem(String text){
this.text=text;
}
@Override
public int getViewType(){
return 0;
@@ -405,6 +425,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
this.onClick=onClick;
}
public TextItem(String text, Runnable onClick){
this.text=text;
this.onClick=onClick;
}
@Override
public int getViewType(){
return 4;
@@ -417,6 +442,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
super(text);
}
public RedHeaderItem(String text){
super(text);
}
@Override
public int getViewType(){
return 5;

View File

@@ -1,20 +1,28 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V;
@@ -23,12 +31,13 @@ public class SplashFragment extends AppKitFragment{
private SizeListenerFrameLayout contentView;
private View artContainer, blueFill, greenFill;
private InterpolatingMotionEffect motionEffect;
private ViewPager2 pager;
private ViewGroup pagerDots;
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
}
@Nullable
@@ -37,15 +46,44 @@ public class SplashFragment extends AppKitFragment{
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);
artClouds=contentView.findViewById(R.id.art_clouds);
artPlaneElephant=contentView.findViewById(R.id.art_plane_elephant);
artRightHill=contentView.findViewById(R.id.art_right_hill);
artLeftHill=contentView.findViewById(R.id.art_left_hill);
artCenterHill=contentView.findViewById(R.id.art_center_hill);
pager=contentView.findViewById(R.id.pager);
pagerDots=contentView.findViewById(R.id.pager_dots);
pager.setAdapter(new PagerAdapter());
pager.setOffscreenPageLimit(3);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
for(int i=0;i<pagerDots.getChildCount();i++){
float alpha;
if(i==position){
alpha=0.3f+0.7f*(1f-positionOffset);
}else if(i==position+1){
alpha=0.3f+0.7f*positionOffset;
}else{
alpha=0.3f;
}
pagerDots.getChildAt(i).setAlpha(alpha);
}
float parallaxProgress=(position+positionOffset)/2f;
artClouds.setTranslationX(V.dp(-27)*(position>=1 ? 1f : positionOffset));
artPlaneElephant.setTranslationX(V.dp(101.55f)*parallaxProgress);
artLeftHill.setTranslationX(V.dp(-88)*parallaxProgress);
artLeftHill.setTranslationY(V.dp(24)*parallaxProgress);
artRightHill.setTranslationX(V.dp(-88)*parallaxProgress);
artRightHill.setTranslationY(V.dp(-24)*parallaxProgress);
artCenterHill.setTranslationX(V.dp(-40)*parallaxProgress);
}
});
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
@@ -72,10 +110,10 @@ public class SplashFragment extends AppKitFragment{
}
private void updateArtSize(int w, int h){
float scale=w/(float)V.dp(412);
float scale=w/(float)V.dp(360);
artContainer.setScaleX(scale);
artContainer.setScaleY(scale);
blueFill.setScaleY(h/2f);
blueFill.setScaleY(artContainer.getBottom()-V.dp(90));
greenFill.setScaleY(h-artContainer.getBottom()+V.dp(90));
}
@@ -101,15 +139,60 @@ public class SplashFragment extends AppKitFragment{
return true;
}
@Override
protected void onShown(){
super.onShown();
motionEffect.activate();
private class PagerAdapter extends RecyclerView.Adapter<PagerViewHolder>{
@NonNull
@Override
public PagerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new PagerViewHolder(viewType);
}
@Override
public void onBindViewHolder(@NonNull PagerViewHolder holder, int position){}
@Override
public int getItemCount(){
return 3;
}
@Override
public int getItemViewType(int position){
return position;
}
}
@Override
protected void onHidden(){
super.onHidden();
motionEffect.deactivate();
private class PagerViewHolder extends RecyclerView.ViewHolder{
public PagerViewHolder(int page){
super(new LinearLayout(getActivity()));
LinearLayout ll=(LinearLayout) itemView;
ll.setOrientation(LinearLayout.VERTICAL);
int pad=V.dp(16);
ll.setPadding(pad, pad, pad, pad);
ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
TextView title=new TextView(getActivity());
title.setTextAppearance(R.style.m3_headline_medium);
title.setText(switch(page){
case 0 -> getString(R.string.welcome_page1_title);
case 1 -> getString(R.string.welcome_page2_title);
case 2 -> getString(R.string.welcome_page3_title);
default -> throw new IllegalStateException("Unexpected value: "+page);
});
title.setTextColor(0xFF17063B);
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(page==0 ? 46 : 36));
lp.bottomMargin=V.dp(page==0 ? 4 : 14);
ll.addView(title, lp);
TextView text=new TextView(getActivity());
text.setTextAppearance(R.style.m3_body_medium);
text.setText(switch(page){
case 0 -> R.string.welcome_page1_text;
case 1 -> R.string.welcome_page2_text;
case 2 -> R.string.welcome_page3_text;
default -> throw new IllegalStateException("Unexpected value: "+page);
});
text.setTextColor(0xFF17063B);
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
}
}

View File

@@ -17,11 +17,11 @@ public class DiscoverPostsFragment extends StatusListFragment{
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetTrendingStatuses(count)
currentRequest=new GetTrendingStatuses(offset, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, false);
onDataLoaded(result, !result.isEmpty());
}
}).exec(accountID);
}

View File

@@ -1,8 +1,8 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -12,19 +12,21 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.MainActivity;
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.AccountActivationInfo;
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.SettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
@@ -35,40 +37,50 @@ 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.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends AppKitFragment{
public class AccountActivationFragment extends ToolbarFragment{
private String accountID;
private Button btn, backBtn;
private View buttonBar;
private Button openEmailBtn, resendBtn;
private View contentView;
private Handler uiHandler=new Handler(Looper.getMainLooper());
private Runnable pollRunnable=this::tryGetAccount;
private APIRequest currentRequest;
private Runnable resendTimer=this::updateResendTimer;
private long lastResendTime;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setTitle(R.string.confirm_email_title);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
lastResendTime=session.activationInfo!=null ? session.activationInfo.lastEmailConfirmationResend : 0;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(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());
btn.setOnLongClickListener(v->{
openEmailBtn=view.findViewById(R.id.btn_next);
openEmailBtn.setOnClickListener(this::onOpenEmailClick);
openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
return true;
});
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->onBackButtonClick());
resendBtn=view.findViewById(R.id.btn_resend);
resendBtn.setOnClickListener(this::onResendClick);
TextView text=view.findViewById(R.id.subtitle);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
text.setText(getString(R.string.confirm_email_subtitle, session.activationInfo!=null ? session.activationInfo.email : "?"));
updateResendTimer();
contentView=view;
return view;
}
@@ -80,14 +92,32 @@ public class AccountActivationFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
@Override
protected boolean canGoBack(){
return true;
}
@Override
public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity()).show();
}
@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);
contentView.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()));
@@ -111,7 +141,7 @@ public class AccountActivationFragment extends AppKitFragment{
}
}
private void onButtonClick(){
private void onOpenEmailClick(View v){
try{
startActivity(Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_EMAIL).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}catch(ActivityNotFoundException x){
@@ -119,12 +149,21 @@ public class AccountActivationFragment extends AppKitFragment{
}
}
private void onBackButtonClick(){
private void onResendClick(View v){
new ResendConfirmationEmail(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
Toast.makeText(getActivity(), R.string.resent_email, Toast.LENGTH_SHORT).show();
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(session.activationInfo==null){
session.activationInfo=new AccountActivationInfo("?", System.currentTimeMillis());
}else{
session.activationInfo.lastEmailConfirmationResend=System.currentTimeMillis();
}
lastResendTime=session.activationInfo.lastEmailConfirmationResend;
AccountSessionManager.getInstance().writeAccountsFile();
updateResendTimer();
}
@Override
@@ -152,32 +191,26 @@ public class AccountActivationFragment extends AppKitFragment{
AccountSessionManager mgr=AccountSessionManager.getInstance();
AccountSession session=mgr.getAccount(accountID);
mgr.removeAccount(accountID);
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, true);
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, null);
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())
accountID=newID;
if((session.self.avatar!=null || session.self.displayName!=null) && !getArguments().getBoolean("debug")){
new UpdateAccountCredentials(session.self.displayName, "", (File)null, 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);
proceed();
}
@Override
public void onError(ErrorResponse error){
if(avaFile!=null)
avaFile.delete();
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
proceed();
}
})
.exec(newID);
}else{
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
proceed();
}
}
@@ -189,4 +222,32 @@ public class AccountActivationFragment extends AppKitFragment{
})
.exec(accountID);
}
@SuppressLint("DefaultLocale")
private void updateResendTimer(){
long sinceResend=System.currentTimeMillis()-lastResendTime;
if(sinceResend>59_000L){
resendBtn.setText(R.string.resend);
resendBtn.setEnabled(true);
return;
}
int seconds=(int)((60_000L-sinceResend)/1000L);
resendBtn.setText(String.format("%s (%d)", getString(R.string.resend), seconds));
if(resendBtn.isEnabled())
resendBtn.setEnabled(false);
resendBtn.postDelayed(resendTimer, 500);
}
@Override
public void onDestroyView(){
super.onDestroyView();
resendBtn.removeCallbacks(resendTimer);
}
private void proceed(){
Bundle args=new Bundle();
args.putString("account", accountID);
// Nav.goClearingStack(getActivity(), HomeFragment.class, args);
Nav.goClearingStack(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
}
}

View File

@@ -1,9 +1,11 @@
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;
import android.view.View;
import android.view.ViewGroup;
@@ -15,8 +17,10 @@ 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;
import org.jsoup.nodes.Document;
import org.parceler.Parcels;
@@ -33,12 +37,14 @@ 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;
import okhttp3.Callback;
@@ -46,7 +52,7 @@ import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class GoogleMadeMeAddThisFragment extends AppKitFragment{
public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@@ -55,11 +61,15 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
private ArrayList<Item> items=new ArrayList<>();
private Call currentRequest;
private ItemsAdapter itemsAdapter;
private ElevationOnScrollListener onScrollListener;
private static final int SIGNUP_REQUEST=722;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.privacy_policy_title);
}
@Override
@@ -68,7 +78,7 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
items.add(new Item("Mastodon for Android Privacy Policy", "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
items.add(new Item("Mastodon for Android Privacy Policy", getString(R.string.privacy_policy_explanation), "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
loadServerPrivacyPolicy();
}
@@ -82,37 +92,30 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(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);
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
title.setText(R.string.privacy_policy_title);
subtitle.setText(R.string.privacy_policy_subtitle);
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));
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
list.setAdapter(adapter);
list.setSelector(null);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.getChildViewHolder(view) instanceof ItemViewHolder){
outRect.left=outRect.right=V.dp(18.5f);
outRect.top=V.dp(16);
}
}
});
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));
Button backBtn=view.findViewById(R.id.btn_back);
backBtn.setText(getString(R.string.server_policy_disagree, instance.uri));
backBtn.setOnClickListener(v->{
setResult(false, null);
Nav.finish(this);
});
return view;
}
@@ -120,13 +123,34 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), SignupFragment.class, args);
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
super.onFragmentResult(reqCode, success, result);
if(reqCode==SIGNUP_REQUEST && !success){
setResult(false, null);
Nav.finish(this);
}
}
@Override
@@ -159,7 +183,7 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
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(), instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
final Item item=new Item(doc.title(), null, instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
Activity activity=getActivity();
if(activity!=null){
activity.runOnUiThread(()->{
@@ -192,24 +216,24 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
}
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView domain, title;
private final ImageView favicon;
private final TextView title;
private final TextView subtitle;
public ItemViewHolder(){
super(getActivity(), R.layout.item_privacy_policy_link, list);
domain=findViewById(R.id.domain);
title=findViewById(R.id.title);
favicon=findViewById(R.id.favicon);
itemView.setOutlineProvider(OutlineProviders.roundedRect(10));
itemView.setClipToOutline(true);
subtitle=findViewById(R.id.subtitle);
}
@Override
public void onBind(Item item){
domain.setText(item.domain);
title.setText(item.title);
ViewImageLoader.load(favicon, null, new UrlImageLoaderRequest(item.faviconUrl));
if(TextUtils.isEmpty(item.subtitle)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.subtitle);
}
}
@Override
@@ -219,10 +243,11 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
}
private static class Item{
public String title, domain, url, faviconUrl;
public String title, subtitle, domain, url, faviconUrl;
public Item(String title, String domain, String url, String faviconUrl){
public Item(String title, String subtitle, String domain, String url, String faviconUrl){
this.title=title;
this.subtitle=subtitle;
this.domain=domain;
this.url=url;
this.faviconUrl=faviconUrl;

View File

@@ -5,15 +5,12 @@ import android.app.ProgressDialog;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.TextUtils;
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.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -23,7 +20,6 @@ import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -31,6 +27,8 @@ import org.xml.sax.InputSource;
import java.io.IOException;
import java.net.IDN;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -50,7 +48,6 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@@ -92,7 +89,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
@@ -106,52 +103,16 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
updateFilteredList();
loadInstanceInfo(currentSearchQuery, false);
}
protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){
Map<String, List<CatalogInstance>> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language));
for(List<CatalogInstance> group:byLang.values()){
Collections.sort(group, (a, b)->{
double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers));
double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers));
return Double.compare(aa, bb);
});
}
// get the list of user-configured system languages
List<String> userLangs;
if(Build.VERSION.SDK_INT<24){
userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage());
}else{
LocaleList ll=getResources().getConfiguration().getLocales();
userLangs=new ArrayList<>(ll.size());
for(int i=0;i<ll.size();i++){
userLangs.add(ll.get(i).getLanguage());
}
}
// add instances in preferred languages to the top of the list, in the order of preference
Map<Boolean, List<CatalogInstance>> byLang=result.stream().sorted(Comparator.comparingInt((CatalogInstance ci)->ci.lastWeekUsers).reversed()).collect(Collectors.groupingBy(ci->ci.approvalRequired));
ArrayList<CatalogInstance> sortedList=new ArrayList<>();
for(String lang:userLangs){
List<CatalogInstance> langInstances=byLang.remove(lang);
if(langInstances!=null){
sortedList.addAll(langInstances);
}
}
// sort the remaining language groups by aggregate lastWeekUsers
class InstanceGroup{
public int activeUsers;
public List<CatalogInstance> instances;
}
byLang.values().stream().map(il->{
InstanceGroup group=new InstanceGroup();
group.instances=il;
for(CatalogInstance instance:il){
group.activeUsers+=instance.lastWeekUsers;
}
return group;
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
sortedList.addAll(byLang.getOrDefault(false, Collections.emptyList()));
sortedList.addAll(byLang.getOrDefault(true, Collections.emptyList()));
return sortedList;
}
@@ -187,6 +148,8 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
if(TextUtils.isEmpty(_domain))
return;
String domain=normalizeInstanceDomain(_domain);
Instance cachedInstance=instancesCache.get(domain);
if(cachedInstance!=null){
@@ -206,6 +169,20 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
cancelLoadingInstanceInfo();
}
}
try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
ivh.rebind();
}
}
}
return;
}
loadingInstanceDomain=domain;
loadingInstanceRequest=new GetInstance();
loadingInstanceRequest.setCallback(new Callback<>(){
@@ -222,7 +199,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
for(CatalogInstance ci : filteredData){
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain) && ci!=fakeInstance){
found=true;
break;

View File

@@ -1,39 +1,46 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
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.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Random;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@@ -42,18 +49,33 @@ 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.OnBackPressedListener;
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 InstanceCatalogSignupFragment extends InstanceCatalogFragment{
private View headerView;
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{
private MastodonAPIRequest<?> getCategoriesRequest;
private TabLayout categoriesList;
private String currentCategory="all";
private List<CatalogCategory> categories=new ArrayList<>();
private View topBar;
private List<String> languages=Collections.emptyList();
private PopupMenu langFilterMenu, speedFilterMenu;
private SignupSpeedFilter currentSignupSpeedFilter=SignupSpeedFilter.ANY;
private String currentLanguage=null;
private boolean searchQueryMode;
private LinearLayout filtersWrap;
private HorizontalScrollView filtersScroll;
private ImageButton backBtn, clearSearchBtn;
private View focusThing;
private FilterChipView categoryGeneral, categorySpecialInterests;
private List<FilterChipView> regionalFilters;
private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
@@ -63,6 +85,12 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
public void onAttach(Context context){
super.onAttach(context);
setRefreshEnabled(false);
setRetainInstance(true);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
loadData();
}
@@ -75,6 +103,25 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
if(getActivity()==null)
return;
onDataLoaded(sortInstances(result), false);
if(langFilterMenu!=null){
Menu menu=langFilterMenu.getMenu();
menu.clear();
menu.add(0, 0, 0, R.string.server_filter_any_language);
languages=result.stream().map(i->i.language).distinct().filter(s->s.length()>0).sorted().collect(Collectors.toList());
int i=1;
for(String lang:languages){
Locale locale=Locale.forLanguageTag(lang);
String name=locale.getDisplayLanguage(locale);
if(name.equals(lang))
name=lang.toUpperCase();
else
name=name.substring(0, 1).toUpperCase()+name.substring(1);
menu.add(0, i, 0, name);
i++;
}
}
updateFilteredList();
}
@@ -111,14 +158,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
}
private void updateCategories(){
categoriesList.removeAllTabs();
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);
}
// categoriesList.removeAllTabs();
// 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
@@ -130,27 +177,37 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
@Override
protected RecyclerView.Adapter getAdapter(){
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();
}
View headerView=new View(getActivity());
headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
backBtn=view.findViewById(R.id.btn_back);
backBtn.setOnClickListener(v->{
if(searchQueryMode){
setSearchQueryMode(false);
}else{
Nav.finish(this);
}
});
clearSearchBtn=view.findViewById(R.id.clear);
clearSearchBtn.setOnClickListener(v->searchEdit.setText(""));
nextButton.setEnabled(true);
list.setItemAnimator(new BetterItemAnimator());
setStatusBarColor(0);
topBar=view.findViewById(R.id.top_bar);
list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar));
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
@@ -166,42 +223,162 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
@Override
public void afterTextChanged(Editable s){
if((clearSearchBtn.getVisibility()==View.VISIBLE)!=(s.length()>0)){
clearSearchBtn.setVisibility(s.length()>0 ? View.VISIBLE : View.GONE);
}
}
});
searchEdit.setOnFocusChangeListener((v, hasFocus)->{
if(hasFocus && !searchQueryMode){
setSearchQueryMode(true);
}
});
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
FilterChipView langFilter=new FilterChipView(getActivity());
langFilter.setDrawableEnd(R.drawable.ic_baseline_arrow_drop_down_18);
if(currentLanguage==null){
langFilter.setText(R.string.server_filter_any_language);
}else{
Locale locale=Locale.forLanguageTag(currentLanguage);
langFilter.setText(locale.getDisplayLanguage(locale));
langFilter.setSelected(true);
}
langFilterMenu=new PopupMenu(getContext(), langFilter);
langFilter.setOnTouchListener(langFilterMenu.getDragToOpenListener());
langFilter.setOnClickListener(v->langFilterMenu.show());
filtersWrap=view.findViewById(R.id.filters_container);
filtersScroll=view.findViewById(R.id.filters_scroll);
filtersWrap.addView(langFilter, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
FilterChipView speedFilter=new FilterChipView(getActivity());
speedFilter.setDrawableEnd(R.drawable.ic_baseline_arrow_drop_down_18);
speedFilterMenu=new PopupMenu(getContext(), speedFilter);
speedFilterMenu.getMenu().add(0, 0, 0, R.string.server_filter_any_signup_speed);
speedFilterMenu.getMenu().add(0, 1, 0, R.string.server_filter_instant_signup);
speedFilterMenu.getMenu().add(0, 2, 0, R.string.server_filter_manual_review);
speedFilter.setOnTouchListener(speedFilterMenu.getDragToOpenListener());
speedFilter.setOnClickListener(v->speedFilterMenu.show());
speedFilter.setText(switch(currentSignupSpeedFilter){
case ANY -> R.string.server_filter_any_signup_speed;
case INSTANT -> R.string.server_filter_instant_signup;
case REVIEWED -> R.string.server_filter_manual_review;
});
speedFilter.setSelected(currentSignupSpeedFilter!=SignupSpeedFilter.ANY);
filtersWrap.addView(speedFilter, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
speedFilterMenu.setOnMenuItemClickListener(item->{
speedFilter.setText(item.getTitle());
speedFilter.setSelected(item.getItemId()>0);
currentSignupSpeedFilter=SignupSpeedFilter.values()[item.getItemId()];
updateFilteredList();
return true;
});
langFilterMenu.setOnMenuItemClickListener(item->{
langFilter.setText(item.getTitle());
langFilter.setSelected(item.getItemId()>0);
currentLanguage=item.getItemId()==0 ? null : languages.get(item.getItemId()-1);
updateFilteredList();
return true;
});
View divider=new View(getActivity());
divider.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
filtersWrap.addView(divider, new LinearLayout.LayoutParams(V.dp(.5f), ViewGroup.LayoutParams.MATCH_PARENT));
categoryGeneral=new FilterChipView(getActivity());
categoryGeneral.setText(R.string.category_general);
categoryGeneral.setTag(CategoryChoice.GENERAL);
categoryGeneral.setOnClickListener(this::onCategoryFilterClick);
categoryGeneral.setSelected(categoryChoice==CategoryChoice.GENERAL);
filtersWrap.addView(categoryGeneral, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
categorySpecialInterests=new FilterChipView(getActivity());
categorySpecialInterests.setText(R.string.category_special_interests);
categorySpecialInterests.setTag(CategoryChoice.SPECIAL);
categorySpecialInterests.setOnClickListener(this::onCategoryFilterClick);
categorySpecialInterests.setSelected(categoryChoice==CategoryChoice.SPECIAL);
filtersWrap.addView(categorySpecialInterests, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
regionalFilters=Arrays.stream(CatalogInstance.Region.values()).map(r->{
FilterChipView fv=new FilterChipView(getActivity());
fv.setTag(r);
fv.setText(switch(r){
case EUROPE -> R.string.server_filter_region_europe;
case NORTH_AMERICA -> R.string.server_filter_region_north_america;
case SOUTH_AMERICA -> R.string.server_filter_region_south_america;
case AFRICA -> R.string.server_filter_region_africa;
case ASIA -> R.string.server_filter_region_asia;
case OCEANIA -> R.string.server_filter_region_oceania;
});
fv.setSelected(r==chosenRegion);
fv.setOnClickListener(this::onRegionFilterClick);
filtersWrap.addView(fv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
return fv;
}).collect(Collectors.toList());
focusThing=view.findViewById(R.id.focus_thing);
focusThing.requestFocus();
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
nextButton.setEnabled(chosenInstance!=null);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
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));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
private void onRegionFilterClick(View v){
CatalogInstance.Region r=(CatalogInstance.Region) v.getTag();
if(chosenRegion==r){
chosenRegion=null;
v.setSelected(false);
}else{
if(chosenRegion!=null)
filtersWrap.findViewWithTag(chosenRegion).setSelected(false);
chosenRegion=r;
v.setSelected(true);
}
updateFilteredList();
}
private void onCategoryFilterClick(View v){
CategoryChoice c=(CategoryChoice) v.getTag();
if(categoryChoice==c){
categoryChoice=null;
v.setSelected(false);
}else{
if(categoryChoice!=null)
filtersWrap.findViewWithTag(categoryChoice).setSelected(false);
categoryChoice=c;
v.setSelected(true);
}
updateFilteredList();
}
@Override
protected void proceedWithAuthOrSignup(Instance instance){
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(isSignup){
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}else{
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
private void onPickRandomInstanceClick(View v){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
onNextClick(v);
}
// private String getEmojiForCategory(String category){
@@ -265,11 +442,29 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
protected void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
for(CatalogInstance instance:data){
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){
for(CatalogInstance instance:data){
if(instance.domain.contains(currentSearchQuery)){
filteredData.add(instance);
}
}
}
}else{
for(CatalogInstance instance:data){
if(categoryChoice==null || categoryChoice.matches(instance.category)){
if(chosenRegion==null || instance.region==chosenRegion){
boolean signupSpeedMatches=switch(currentSignupSpeedFilter){
case ANY -> true;
case INSTANT -> !instance.approvalRequired;
case REVIEWED -> instance.approvalRequired;
};
if(signupSpeedMatches){
if(currentLanguage==null || instance.languages.contains(currentLanguage)){
filteredData.add(instance);
}
}
}
}
}
}
@@ -296,6 +491,53 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
}).dispatchUpdatesTo(adapter);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
topBar.setPadding(0, insets.getSystemWindowInsetTop(), 0, 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
@Override
public boolean onBackPressed(){
if(searchQueryMode){
setSearchQueryMode(false);
return true;
}
return false;
}
private void setSearchQueryMode(boolean enabled){
searchQueryMode=enabled;
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams();
if(searchQueryMode){
filtersScroll.setVisibility(View.GONE);
lp.removeRule(RelativeLayout.END_OF);
backBtn.setScaleX(0.83333333f);
backBtn.setScaleY(0.83333333f);
backBtn.setTranslationX(V.dp(8));
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0));
}else{
filtersScroll.setVisibility(View.VISIBLE);
focusThing.requestFocus();
searchEdit.setText("");
lp.addRule(RelativeLayout.END_OF, R.id.btn_back);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
backBtn.setScaleX(1);
backBtn.setScaleY(1);
backBtn.setTranslationX(0);
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant)));
}
updateFilteredList();
}
@Override
protected void onShown(){
super.onShown();
if(!searchQueryMode){
// Prevent search view automatically getting focused when the user returns to this fragment
focusThing.requestFocus();
}
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
public InstancesAdapter(){
@@ -325,30 +567,36 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description, userCount, lang;
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.DisableableClickable{
private final TextView title, description;
private final RadioButton radioButton;
private boolean enabled;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_catalog, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
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(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
radioButton.setChecked(chosenInstance==item);
Instance realInstance=instancesCache.get(item.normalizedDomain);
float alpha;
if(realInstance!=null && !realInstance.registrations){
alpha=0.38f;
description.setText(R.string.not_accepting_new_members);
enabled=false;
}else{
alpha=1f;
description.setText(item.description);
enabled=true;
}
title.setAlpha(alpha);
description.setAlpha(alpha);
radioButton.setAlpha(alpha);
}
@Override
@@ -358,17 +606,49 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
if(holder instanceof InstanceCatalogSignupFragment.InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
boolean found=false;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
found=true;
break;
}
}
if(!found)
adapter.notifyItemChanged(idx);
}
}
if(!nextButton.isEnabled()){
nextButton.setEnabled(true);
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
}
@Override
public boolean isEnabled(){
return enabled;
}
}
private enum SignupSpeedFilter{
ANY,
INSTANT,
REVIEWED
}
private enum CategoryChoice{
GENERAL,
SPECIAL;
public boolean matches(String category){
boolean isGeneral=(category==null || "general".equals(category));
return (this==GENERAL)==isGeneral;
}
}
}

View File

@@ -240,13 +240,17 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
boolean found=false;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
found=true;
break;
}
}
if(!found)
adapter.notifyItemChanged(idx);
}
}
radioButton.setChecked(true);

View File

@@ -1,8 +1,15 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -16,25 +23,30 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
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.fragments.ToolbarFragment;
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;
public class InstanceRulesFragment extends AppKitFragment{
public class InstanceRulesFragment extends ToolbarFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
private View buttonBar;
private Instance instance;
private ElevationOnScrollListener onScrollListener;
private static final int RULES_REQUEST=376;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -47,30 +59,29 @@ public class InstanceRulesFragment extends AppKitFragment{
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
setTitle(R.string.instance_rules_title);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(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);
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
title.setText(R.string.instance_rules_title);
subtitle.setText(getString(R.string.instance_rules_subtitle, instance.uri));
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>")));
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));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, 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;
@@ -79,13 +90,33 @@ public class InstanceRulesFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), GoogleMadeMeAddThisFragment.class, args);
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
super.onFragmentResult(reqCode, success, result);
if(reqCode==RULES_REQUEST && !success){
Nav.finish(this);
}
}
@Override
@@ -119,23 +150,22 @@ public class InstanceRulesFragment extends AppKitFragment{
}
private class ItemViewHolder extends BindableViewHolder<Instance.Rule>{
private final TextView title, subtitle;
private final ImageView checkbox;
private final TextView text, number;
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);
super(getActivity(), R.layout.item_server_rule, list);
text=findViewById(R.id.text);
number=findViewById(R.id.number);
}
@SuppressLint("DefaultLocale")
@Override
public void onBind(Instance.Rule item){
if(item.parsedText==null){
item.parsedText=HtmlParser.parseLinks(item.text);
}
title.setText(item.parsedText);
text.setText(item.parsedText);
number.setText(String.format("%d", getAbsoluteAdapterPosition()));
}
}
}

View File

@@ -0,0 +1,344 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.ProgressDialog;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
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.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.ParsedAccount;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
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.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0;
public OnboardingFollowSuggestionsFragment(){
super(R.layout.fragment_onboarding_follow_suggestions, 40);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.popular_on_mastodon);
accountID=getArguments().getString("account");
loadData();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
@Override
protected void doLoadData(int offset, int count){
new GetFollowSuggestions(40)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
loadRelationships();
}
})
.exec(accountID);
}
private void loadRelationships(){
relationships=Collections.emptyMap();
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
relationshipsRequest.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationshipsRequest=null;
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof SuggestionViewHolder svh)
svh.rebind();
}
}
@Override
public void onError(ErrorResponse error){
relationshipsRequest=null;
}
}).exec(accountID);
}
@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 RecyclerView.Adapter getAdapter(){
return new SuggestionsAdapter();
}
private void onFollowAllClick(View v){
if(!loaded || relationships.isEmpty())
return;
if(data.isEmpty()){
proceed();
return;
}
ArrayList<String> accountIdsToFollow=new ArrayList<>();
for(ParsedAccount acc:data){
Relationship rel=relationships.get(acc.account.id);
if(rel==null)
continue;
if(rel.canFollow())
accountIdsToFollow.add(acc.account.id);
}
final ProgressDialog progress=new ProgressDialog(getActivity());
progress.setIndeterminate(false);
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progress.setMax(accountIdsToFollow.size());
progress.setCancelable(false);
progress.setMessage(getString(R.string.sending_follows));
progress.show();
for(int i=0;i<Math.min(accountIdsToFollow.size(), 5);i++){ // Send up to 5 requests in parallel
followNextAccount(accountIdsToFollow, progress);
}
}
private void followNextAccount(ArrayList<String> accountIdsToFollow, ProgressDialog progress){
if(accountIdsToFollow.isEmpty()){
if(numRunningFollowRequests==0){
progress.dismiss();
proceed();
}
return;
}
numRunningFollowRequests++;
String id=accountIdsToFollow.remove(0);
new SetAccountFollowed(id, true, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
relationships.put(id, result);
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof SuggestionViewHolder svh && svh.getItem().account.id.equals(id)){
svh.rebind();
break;
}
}
numRunningFollowRequests--;
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
followNextAccount(accountIdsToFollow, progress);
}
@Override
public void onError(ErrorResponse error){
numRunningFollowRequests--;
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
followNextAccount(accountIdsToFollow, progress);
}
})
.exec(accountID);
}
private void proceed(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), OnboardingProfileSetupFragment.class, args);
}
private class SuggestionsAdapter extends UsableRecyclerView.Adapter<SuggestionViewHolder> implements ImageLoaderRecyclerAdapter{
public SuggestionsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new SuggestionViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public void onBindViewHolder(SuggestionViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return data.get(position).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
ParsedAccount account=data.get(position);
if(image==0)
return account.avatarRequest;
return account.emojiHelper.getImageRequest(image-1);
}
}
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name, username, bio;
private final ImageView avatar;
private final ProgressBarButton actionButton;
private final ProgressBar actionProgress;
private final View actionWrap;
private Relationship relationship;
public SuggestionViewHolder(){
super(getActivity(), R.layout.item_user_row_m3, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
bio=findViewById(R.id.bio);
avatar=findViewById(R.id.avatar);
actionButton=findViewById(R.id.action_btn);
actionProgress=findViewById(R.id.action_progress);
actionWrap=findViewById(R.id.action_btn_wrap);
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
avatar.setClipToOutline(true);
actionButton.setOnClickListener(UiUtils.rateLimitedClickListener(this::onActionButtonClick));
}
@Override
public void onBind(ParsedAccount item){
name.setText(item.parsedName);
username.setText(item.account.getDisplayUsername());
if(TextUtils.isEmpty(item.parsedBio)){
bio.setVisibility(View.GONE);
}else{
bio.setVisibility(View.VISIBLE);
bio.setText(item.parsedBio);
}
relationship=relationships.get(item.account.id);
if(relationship==null){
actionWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
bio.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
private void onActionButtonClick(View v){
itemView.setHasTransientState(true);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);
rebind();
});
}
private void setActionProgressVisible(boolean visible){
actionButton.setTextVisible(!visible);
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
actionButton.setClickable(!visible);
}
}
}

View File

@@ -0,0 +1,229 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
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.EditText;
import android.widget.ImageView;
import android.widget.ScrollView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
private Button btn;
private View buttonBar;
private String accountID;
private ElevationOnScrollListener onScrollListener;
private ScrollView scroller;
private EditText nameEdit, bioEdit;
private ImageView avaImage, coverImage;
private Button addRow;
private ReorderableLinearLayout profileFieldsLayout;
private Uri avatarUri, coverUri;
private static final int AVATAR_RESULT=348;
private static final int COVER_RESULT=183;
@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));
accountID=getArguments().getString("account");
setTitle(R.string.profile_setup);
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_profile_setup, container, false);
scroller=view.findViewById(R.id.scroller);
nameEdit=view.findViewById(R.id.display_name);
bioEdit=view.findViewById(R.id.bio);
avaImage=view.findViewById(R.id.avatar);
coverImage=view.findViewById(R.id.header);
addRow=view.findViewById(R.id.add_row);
profileFieldsLayout=view.findViewById(R.id.profile_fields);
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
avaImage.setOutlineProvider(OutlineProviders.roundedRect(24));
avaImage.setClipToOutline(true);
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
if(savedInstanceState==null){
nameEdit.setText(account.displayName);
makeFieldsRow();
}else{
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
for(int i=0;i<fieldTitles.size();i++){
View row=makeFieldsRow();
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
title.setText(fieldTitles.get(i));
content.setText(fieldValues.get(i));
}
if(fieldTitles.size()==4)
addRow.setVisibility(View.GONE);
}
addRow.setOnClickListener(v->{
makeFieldsRow();
if(profileFieldsLayout.getChildCount()==4){
addRow.setVisibility(View.GONE);
}
});
profileFieldsLayout.setDragListener(this);
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
scroller.setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
protected void onButtonClick(){
ArrayList<AccountField> fields=new ArrayList<>();
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
AccountField fld=new AccountField();
fld.name=title.getText().toString();
fld.value=content.getText().toString();
fields.add(fld);
}
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account result){
AccountSessionManager.getInstance().updateAccountInfo(accountID, result);
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.saving, true)
.exec(accountID);
}
@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 View makeFieldsRow(){
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
profileFieldsLayout.addView(view);
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
profileFieldsLayout.startDragging(view);
return true;
});
view.findViewById(R.id.delete).setOnClickListener(v->{
profileFieldsLayout.removeView(view);
if(addRow.getVisibility()==View.GONE)
addRow.setVisibility(View.VISIBLE);
});
return view;
}
@Override
public void onSwapItems(int oldIndex, int newIndex){}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
View row=profileFieldsLayout.getChildAt(i);
EditText title=row.findViewById(R.id.title);
EditText content=row.findViewById(R.id.content);
fieldTitles.add(title.getText().toString());
fieldValues.add(content.getText().toString());
}
outState.putStringArrayList("fieldTitles", fieldTitles);
outState.putStringArrayList("fieldValues", fieldValues);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode!=Activity.RESULT_OK)
return;
ImageView img;
Uri uri=data.getData();
int size;
if(requestCode==AVATAR_RESULT){
img=avaImage;
avatarUri=uri;
size=V.dp(100);
}else{
img=coverImage;
coverUri=uri;
size=V.dp(1000);
}
img.setForeground(null);
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
}
}

View File

@@ -1,14 +1,18 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -16,32 +20,35 @@ 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 org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonDetailedErrorResponse;
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.AccountActivationInfo;
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.text.LinkSpan;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeVisitor;
import org.parceler.Parcels;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import androidx.annotation.Nullable;
@@ -49,31 +56,28 @@ 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.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class SignupFragment extends AppKitFragment{
private static final int AVATAR_RESULT=198;
public class SignupFragment extends ToolbarFragment{
private static final String TAG="SignupFragment";
private Instance instance;
private EditText displayName, username, email, password, reason;
private EditText displayName, username, email, password, passwordConfirm, reason;
private FloatingHintEditTextLayout displayNameWrap, usernameWrap, emailWrap, passwordWrap, passwordConfirmWrap, reasonWrap;
private TextView reasonExplain;
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;
private HashSet<EditText> errorFields=new HashSet<>();
private ElevationOnScrollListener onScrollListener;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -81,25 +85,30 @@ public class SignupFragment extends AppKitFragment{
setRetainInstance(true);
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken();
setTitle(R.string.signup_title);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(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);
passwordConfirm=view.findViewById(R.id.password_confirm);
reason=view.findViewById(R.id.reason);
reasonExplain=view.findViewById(R.id.reason_explain);
View avaWrap=view.findViewById(R.id.ava_wrap);
title.setText(getString(R.string.signup_title, instance.uri));
displayNameWrap=view.findViewById(R.id.display_name_wrap);
usernameWrap=view.findViewById(R.id.username_wrap);
emailWrap=view.findViewById(R.id.email_wrap);
passwordWrap=view.findViewById(R.id.password_wrap);
passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap);
reasonWrap=view.findViewById(R.id.reason_wrap);
domain.setText('@'+instance.uri);
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@@ -114,23 +123,20 @@ public class SignupFragment extends AppKitFragment{
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);
passwordConfirm.addTextChangedListener(buttonStateUpdater);
reason.addTextChangedListener(buttonStateUpdater);
username.addTextChangedListener(new ErrorClearingListener(username));
email.addTextChangedListener(new ErrorClearingListener(email));
password.addTextChangedListener(new ErrorClearingListener(password));
passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm));
reason.addTextChangedListener(new ErrorClearingListener(reason));
avaWrap.setOutlineProvider(OutlineProviders.roundedRect(22));
avaWrap.setClipToOutline(true);
avaWrap.setOnClickListener(v->onAvatarClick());
if(!instance.approvalRequired){
reason.setVisibility(View.GONE);
reasonExplain.setVisibility(View.GONE);
@@ -142,10 +148,26 @@ public class SignupFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.findViewById(R.id.scroller).setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
}
private void onButtonClick(){
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match));
return;
}
showProgressDialog();
if(currentBackgroundRequest!=null){
submitAfterGettingToken=true;
@@ -160,32 +182,8 @@ public class SignupFragment extends AppKitFragment{
}
}
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();
}
actuallySubmit();
}
private void actuallySubmit(){
@@ -204,9 +202,7 @@ public class SignupFragment extends AppKitFragment{
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);
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, new AccountActivationInfo(email, System.currentTimeMillis()));
Bundle args=new Bundle();
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
@@ -224,7 +220,22 @@ public class SignupFragment extends AppKitFragment{
anyFieldsSkipped=true;
continue;
}
field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n")));
List<MastodonDetailedErrorResponse.FieldError> errors=Objects.requireNonNull(fieldErrors.get(fieldName));
if(errors.size()==1){
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
}else{
SpannableStringBuilder ssb=new SpannableStringBuilder();
boolean firstErr=true;
for(MastodonDetailedErrorResponse.FieldError err:errors){
if(firstErr){
firstErr=false;
}else{
ssb.append('\n');
}
ssb.append(getErrorDescription(err, fieldName));
}
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
}
errorFields.add(field);
if(first){
first=false;
@@ -242,6 +253,40 @@ public class SignupFragment extends AppKitFragment{
.exec(instance.uri, apiToken);
}
private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){
return switch(fieldName){
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)));
SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){
private int spanStart;
@Override
public void head(Node node, int depth){
if(node instanceof TextNode tn){
ssb.append(tn.text());
}else if(node instanceof Element){
spanStart=ssb.length();
}
}
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
yield ssb;
}
default -> error.description;
};
default -> error.description;
};
}
private EditText getFieldByName(String name){
return switch(name){
case "email" -> email;
@@ -252,6 +297,16 @@ public class SignupFragment extends AppKitFragment{
};
}
private FloatingHintEditTextLayout getFieldWrapByName(String name){
return switch(name){
case "email" -> emailWrap;
case "username" -> usernameWrap;
case "password" -> passwordWrap;
case "reason" -> reasonWrap;
default -> null;
};
}
private void showProgressDialog(){
if(progressDialog==null){
progressDialog=new ProgressDialog(getActivity());
@@ -262,7 +317,7 @@ public class SignupFragment extends AppKitFragment{
}
private void updateButtonState(){
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && (!instance.approvalRequired || reason.length()>0));
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0));
}
private void createAppAndGetToken(){
@@ -324,18 +379,9 @@ public class SignupFragment extends AppKitFragment{
}
}
@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);
private void onGoBackLinkClick(LinkSpan span){
setResult(false, null);
Nav.finish(this);
}
private class ErrorClearingListener implements TextWatcher{

View File

@@ -45,26 +45,26 @@ public class Attachment extends BaseModel{
public int getWidth(){
if(meta==null)
return 0;
return 1920;
if(meta.width>0)
return meta.width;
if(meta.original!=null && meta.original.width>0)
return meta.original.width;
if(meta.small!=null && meta.small.width>0)
return meta.small.width;
return 0;
return 1920;
}
public int getHeight(){
if(meta==null)
return 0;
return 1080;
if(meta.height>0)
return meta.height;
if(meta.original!=null && meta.original.height>0)
return meta.original.height;
if(meta.small!=null && meta.small.height>0)
return meta.small.height;
return 0;
return 1080;
}
public double getDuration(){

View File

@@ -45,7 +45,7 @@ public class Instance extends BaseModel{
@RequiredField
public String version;
/**
* Primary langauges of the website and its staff.
* Primary languages of the website and its staff.
*/
// @RequiredField
public List<String> languages;

View File

@@ -0,0 +1,33 @@
package org.joinmastodon.android.model;
import android.text.SpannableStringBuilder;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import java.util.Collections;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ParsedAccount{
public Account account;
public CharSequence parsedName, parsedBio;
public CustomEmojiHelper emojiHelper;
public ImageLoaderRequest avatarRequest;
public ParsedAccount(Account account, String accountID){
this.account=account;
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
emojiHelper=new CustomEmojiHelper();
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
ssb.append(parsedBio);
emojiHelper.setText(ssb);
avatarRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(40), V.dp(40));
}
}

View File

@@ -23,6 +23,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
", endpoint='"+endpoint+'\''+
", alerts="+alerts+
", serverKey='"+serverKey+'\''+
", policy="+policy+
'}';
}

View File

@@ -18,6 +18,10 @@ public class Relationship extends BaseModel{
public boolean blockedBy;
public String note;
public boolean canFollow(){
return !(following || blocking || blockedBy || domainBlocking);
}
@Override
public String toString(){
return "Relationship{"+

View File

@@ -1,5 +1,10 @@
package org.joinmastodon.android.model.catalog;
import android.graphics.Region;
import android.text.TextUtils;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.model.BaseModel;
@@ -7,13 +12,17 @@ import org.joinmastodon.android.model.BaseModel;
import java.net.IDN;
import java.util.List;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
@AllFieldsAreRequired
public class CatalogInstance extends BaseModel{
public String domain;
public String version;
public String description;
public List<String> languages;
public String region;
@SerializedName("region")
private String _region;
public List<String> categories;
public String proxiedThumbnail;
public int totalUsers;
@@ -22,7 +31,9 @@ public class CatalogInstance extends BaseModel{
public String language;
public String category;
public transient Region region;
public transient String normalizedDomain;
public transient UrlImageLoaderRequest thumbnailRequest;
@Override
public void postprocess() throws ObjectValidationException{
@@ -31,6 +42,14 @@ public class CatalogInstance extends BaseModel{
normalizedDomain=IDN.toUnicode(domain);
else
normalizedDomain=domain;
if(!TextUtils.isEmpty(_region)){
try{
region=Region.valueOf(_region.toUpperCase());
}catch(IllegalArgumentException ignore){}
}
if(!TextUtils.isEmpty(proxiedThumbnail)){
thumbnailRequest=new UrlImageLoaderRequest(proxiedThumbnail, 0, V.dp(56));
}
}
@Override
@@ -50,4 +69,13 @@ public class CatalogInstance extends BaseModel{
", category='"+category+'\''+
'}';
}
public enum Region{
EUROPE,
NORTH_AMERICA,
SOUTH_AMERICA,
AFRICA,
ASIA,
OCEANIA
}
}

View File

@@ -243,7 +243,10 @@ public class AccountSwitcherSheet extends BottomSheet{
public WrappedAccount(AccountSession session){
this.session=session;
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
if(session.self.avatar!=null)
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
else
req=null;
}
}
}

View File

@@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
@@ -154,11 +155,16 @@ public class ComposeAutocompleteViewController{
}else if(mode==Mode.EMOJIS){
String _text=text.substring(1); // remove ':'
List<WrappedEmoji> oldList=emojis;
emojis=AccountSessionManager.getInstance()
List<Emoji> allEmojis = AccountSessionManager.getInstance()
.getCustomEmojis(AccountSessionManager.getInstance().getAccount(accountID).domain)
.stream()
.flatMap(ec->ec.emojis.stream())
.filter(e->e.visibleInPicker && e.shortcode.startsWith(_text))
.filter(e->e.visibleInPicker)
.collect(Collectors.toList());
List<Emoji> startsWithSearch = allEmojis.stream().filter(e -> e.shortcode.toLowerCase().startsWith(_text.toLowerCase())).collect(Collectors.toList());
emojis=Stream.concat(startsWithSearch.stream(), allEmojis.stream()
.filter(e -> !startsWithSearch.contains(e))
.filter(e -> e.shortcode.toLowerCase().contains(_text.toLowerCase())))
.map(WrappedEmoji::new)
.collect(Collectors.toList());
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));

View File

@@ -1,7 +1,18 @@
package org.joinmastodon.android.ui.displayitems;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ScrollView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
@@ -10,6 +21,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
@@ -23,9 +35,110 @@ public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
}
public static class Holder extends ImageStatusDisplayItem.Holder<PhotoStatusDisplayItem>{
private final FrameLayout altTextWrapper;
private final TextView altTextButton;
private final View altTextScroller;
private final ImageButton altTextClose;
private final TextView altText;
private boolean altTextShown;
private AnimatorSet currentAnim;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_photo, parent);
altTextWrapper=findViewById(R.id.alt_text_wrapper);
altTextButton=findViewById(R.id.alt_button);
altTextScroller=findViewById(R.id.alt_text_scroller);
altTextClose=findViewById(R.id.alt_text_close);
altText=findViewById(R.id.alt_text);
altTextButton.setOnClickListener(this::onShowHideClick);
altTextClose.setOnClickListener(this::onShowHideClick);
// altTextScroller.setNestedScrollingEnabled(true);
}
@Override
public void onBind(ImageStatusDisplayItem item){
super.onBind(item);
altTextShown=false;
if(currentAnim!=null)
currentAnim.cancel();
altTextScroller.setVisibility(View.GONE);
altTextClose.setVisibility(View.GONE);
altTextButton.setVisibility(View.VISIBLE);
altTextButton.setAlpha(1f);
if(TextUtils.isEmpty(item.attachment.description)){
altTextWrapper.setVisibility(View.GONE);
}else{
altTextWrapper.setVisibility(View.VISIBLE);
altText.setText(item.attachment.description);
}
}
private void onShowHideClick(View v){
boolean show=v.getId()==R.id.alt_button;
if(altTextShown==show)
return;
if(currentAnim!=null)
currentAnim.cancel();
altTextShown=show;
if(show){
altTextScroller.setVisibility(View.VISIBLE);
altTextClose.setVisibility(View.VISIBLE);
}else{
altTextButton.setVisibility(View.VISIBLE);
// Hide these views temporarily so FrameLayout measures correctly
altTextScroller.setVisibility(View.GONE);
altTextClose.setVisibility(View.GONE);
}
// This is the current size...
int prevLeft=altTextWrapper.getLeft();
int prevRight=altTextWrapper.getRight();
int prevTop=altTextWrapper.getTop();
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
// ...and this is after the layout pass, right now the FrameLayout has its final size, but we animate that change
if(!show){
// Show these views again so they're visible for the duration of the animation.
// No one would notice they were missing during measure/layout.
altTextScroller.setVisibility(View.VISIBLE);
altTextClose.setVisibility(View.VISIBLE);
}
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(altTextWrapper, "left", prevLeft, altTextWrapper.getLeft()),
ObjectAnimator.ofInt(altTextWrapper, "right", prevRight, altTextWrapper.getRight()),
ObjectAnimator.ofInt(altTextWrapper, "top", prevTop, altTextWrapper.getTop()),
ObjectAnimator.ofFloat(altTextButton, View.ALPHA, show ? 1f : 0f, show ? 0f : 1f),
ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f),
ObjectAnimator.ofFloat(altTextClose, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f)
);
set.setDuration(300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
if(show){
altTextButton.setVisibility(View.GONE);
}else{
altTextScroller.setVisibility(View.GONE);
altTextClose.setVisibility(View.GONE);
}
currentAnim=null;
}
});
set.start();
currentAnim=set;
return true;
}
});
}
}
}

View File

@@ -602,7 +602,7 @@ public class TabLayout extends HorizontalScrollView {
* <p>If the tab indicator color is not {@code Color.TRANSPARENT}, the indicator will be wrapped
* and tinted right before it is drawn by {@link SlidingTabIndicator#draw(Canvas)}. If you'd like
* the inherent color or the tinted color of a custom drawable to be used, make sure this color is
* set to {@code Color.TRANSPARENT} to avoid your color/tint being overriden.
* set to {@code Color.TRANSPARENT} to avoid your color/tint being overridden.
*
* @param color color to use for the indicator
* @attr ref com.google.android.material.R.styleable#TabLayout_tabIndicatorColor

View File

@@ -35,6 +35,7 @@ public class LinkSpan extends CharacterStyle {
case URL -> UiUtils.openURL(context, accountID, link);
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
case HASHTAG -> UiUtils.openHashtagTimeline(context, accountID, link);
case CUSTOM -> listener.onLinkClick(this);
}
}
@@ -57,6 +58,7 @@ public class LinkSpan extends CharacterStyle {
public enum Type{
URL,
MENTION,
HASHTAG
HASHTAG,
CUSTOM
}
}

View File

@@ -20,9 +20,13 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.ext.SdkExtensions;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -442,6 +446,32 @@ public class UiUtils{
ta.recycle();
}
public static void setRelationshipToActionButtonM3(Relationship relationship, Button button){
boolean secondaryStyle;
if(relationship.blocking){
button.setText(R.string.button_blocked);
secondaryStyle=true;
}else if(relationship.blockedBy){
button.setText(R.string.button_follow);
secondaryStyle=false;
}else if(relationship.requested){
button.setText(R.string.button_follow_pending);
secondaryStyle=true;
}else if(!relationship.following){
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
secondaryStyle=false;
}else{
button.setText(R.string.button_following);
secondaryStyle=true;
}
button.setEnabled(!relationship.blockedBy);
int styleRes=secondaryStyle ? R.style.Widget_Mastodon_M3_Button_Tonal : R.style.Widget_Mastodon_M3_Button_Filled;
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0));
ta.recycle();
}
public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback){
if(relationship.blocking){
confirmToggleBlockUser(activity, accountID, account, true, resultCallback);
@@ -587,4 +617,84 @@ public class UiUtils{
}
launchWebBrowser(context, url);
}
private static String getSystemProperty(String key){
try{
Class<?> props=Class.forName("android.os.SystemProperties");
Method get=props.getMethod("get", String.class);
return (String)get.invoke(null, key);
}catch(Exception ignore){}
return null;
}
public static boolean isMIUI(){
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
}
public static int alphaBlendColors(int color1, int color2, float alpha){
float alpha0=1f-alpha;
int r=Math.round(((color1 >> 16) & 0xFF)*alpha0+((color2 >> 16) & 0xFF)*alpha);
int g=Math.round(((color1 >> 8) & 0xFF)*alpha0+((color2 >> 8) & 0xFF)*alpha);
int b=Math.round((color1 & 0xFF)*alpha0+(color2 & 0xFF)*alpha);
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
/**
* Check to see if Android platform photopicker is available on the device\
*
* @return whether the device supports photopicker intents.
*/
@SuppressLint("NewApi")
public static boolean isPhotoPickerAvailable(){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
return true;
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){
return SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R)>=2;
}else
return false;
}
@SuppressLint("InlinedApi")
public static Intent getMediaPickerIntent(String[] mimeTypes, int maxCount){
Intent intent;
if(isPhotoPickerAvailable()){
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
if(maxCount>1)
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount);
}else{
intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
}
if(mimeTypes.length>1){
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
}else if(mimeTypes.length==1){
intent.setType(mimeTypes[0]);
}else{
intent.setType("*/*");
}
if(maxCount>1)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
return intent;
}
/**
* Wraps a View.OnClickListener to filter multiple clicks in succession.
* Useful for buttons that perform some action that changes their state asynchronously.
* @param l
* @return
*/
public static View.OnClickListener rateLimitedClickListener(View.OnClickListener l){
return new View.OnClickListener(){
private long lastClickTime;
@Override
public void onClick(View v){
if(SystemClock.uptimeMillis()-lastClickTime>500L){
lastClickTime=SystemClock.uptimeMillis();
l.onClick(v);
}
}
};
}
}

View File

@@ -0,0 +1,59 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.Button;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.DrawableRes;
import me.grishka.appkit.utils.V;
public class FilterChipView extends Button{
private boolean currentlySelected;
public FilterChipView(Context context){
this(context, null);
}
public FilterChipView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public FilterChipView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
setCompoundDrawablePadding(V.dp(8));
setBackgroundResource(R.drawable.bg_filter_chip);
setTextAppearance(R.style.m3_label_large);
setTextColor(getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface)));
updatePadding();
}
@Override
protected void drawableStateChanged(){
super.drawableStateChanged();
if(currentlySelected==isSelected())
return;
currentlySelected=isSelected();
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()) : null;
Drawable end=getCompoundDrawablesRelative()[2];
setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null);
updatePadding();
}
private void updatePadding(){
int vertical=V.dp(6);
Drawable[] drawables=getCompoundDrawablesRelative();
setPaddingRelative(V.dp(drawables[0]==null ? 16 : 8), vertical, V.dp(drawables[2]==null ? 16 : 8), vertical);
}
public void setDrawableEnd(@DrawableRes int drawable){
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, drawable, 0);
updatePadding();
}
}

View File

@@ -5,19 +5,35 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.text.Editable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
@@ -28,6 +44,11 @@ public class FloatingHintEditTextLayout extends FrameLayout{
private int offsetY;
private boolean hintVisible;
private Animator currentAnim;
private float animProgress;
private RectF tmpRect=new RectF();
private ColorStateList labelColors, origHintColors;
private boolean errorState;
private TextView errorView;
public FloatingHintEditTextLayout(Context context){
this(context, null);
@@ -44,7 +65,9 @@ public class FloatingHintEditTextLayout extends FrameLayout{
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FloatingHintEditTextLayout);
labelTextSize=ta.getDimensionPixelSize(R.styleable.FloatingHintEditTextLayout_android_labelTextSize, V.dp(12));
offsetY=ta.getDimensionPixelOffset(R.styleable.FloatingHintEditTextLayout_editTextOffsetY, 0);
labelColors=ta.getColorStateList(R.styleable.FloatingHintEditTextLayout_labelTextColor);
ta.recycle();
setAddStatesFromChildren(true);
}
@Override
@@ -58,13 +81,15 @@ public class FloatingHintEditTextLayout extends FrameLayout{
label=new TextView(getContext());
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize);
label.setTextColor(edit.getHintTextColors());
// label.setTextColor(labelColors==null ? edit.getHintTextColors() : labelColors);
origHintColors=edit.getHintTextColors();
label.setText(edit.getHint());
label.setSingleLine();
label.setPivotX(0f);
label.setPivotY(0f);
label.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP);
lp.setMarginStart(edit.getPaddingStart());
lp.setMarginStart(edit.getPaddingStart()+((LayoutParams)edit.getLayoutParams()).getMarginStart());
addView(label, lp);
hintVisible=edit.getText().length()==0;
@@ -72,9 +97,24 @@ public class FloatingHintEditTextLayout extends FrameLayout{
label.setAlpha(0f);
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
errorView=new LinkedTextView(getContext());
errorView.setTextAppearance(R.style.m3_body_small);
errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
errorView.setLinkTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
errorView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
errorView.setPadding(V.dp(16), V.dp(4), V.dp(16), 0);
errorView.setVisibility(View.GONE);
addView(errorView);
}
private void onTextChanged(Editable text){
if(errorState){
errorView.setVisibility(View.GONE);
errorState=false;
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field, getContext().getTheme()));
refreshDrawableState();
}
boolean newHintVisible=text.length()==0;
if(newHintVisible==hintVisible)
return;
@@ -83,42 +123,232 @@ public class FloatingHintEditTextLayout extends FrameLayout{
hintVisible=newHintVisible;
label.setAlpha(1);
float scale=edit.getLineHeight()/(float)label.getLineHeight();
float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
AnimatorSet anim=new AnimatorSet();
if(hintVisible){
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(label, SCALE_X, scale),
ObjectAnimator.ofFloat(label, SCALE_Y, scale),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY)
);
edit.setHintTextColor(0);
}else{
label.setScaleX(scale);
label.setScaleY(scale);
label.setTranslationY(transY);
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY),
ObjectAnimator.ofFloat(label, SCALE_X, 1f),
ObjectAnimator.ofFloat(label, SCALE_Y, 1f),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f)
);
}
anim.setDuration(150);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
anim.start();
anim.addListener(new AnimatorListenerAdapter(){
edit.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public void onAnimationEnd(Animator animation){
currentAnim=null;
public boolean onPreDraw(){
edit.getViewTreeObserver().removeOnPreDrawListener(this);
float scale=edit.getLineHeight()/(float)label.getLineHeight();
float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
AnimatorSet anim=new AnimatorSet();
if(hintVisible){
label.setAlpha(0);
edit.setHintTextColor(label.getTextColors());
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(label, SCALE_X, scale),
ObjectAnimator.ofFloat(label, SCALE_Y, scale),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY),
ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 0f)
);
edit.setHintTextColor(0);
}else{
label.setScaleX(scale);
label.setScaleY(scale);
label.setTranslationY(transY);
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY),
ObjectAnimator.ofFloat(label, SCALE_X, 1f),
ObjectAnimator.ofFloat(label, SCALE_Y, 1f),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f),
ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 1f)
);
}
anim.setDuration(150);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
anim.start();
anim.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentAnim=null;
if(hintVisible){
label.setAlpha(0);
edit.setHintTextColor(origHintColors);
}
}
});
currentAnim=anim;
return true;
}
});
currentAnim=anim;
}
@Keep
public void setAnimProgress(float progress){
animProgress=progress;
invalidate();
}
@Keep
public float getAnimProgress(){
return animProgress;
}
@Override
public void onDrawForeground(Canvas canvas){
if(getForeground()!=null && animProgress>0){
canvas.save();
float width=(label.getWidth()+V.dp(8))*animProgress;
float centerX=label.getLeft()+label.getWidth()/2f;
tmpRect.set(centerX-width/2f, label.getTop(), centerX+width/2f, label.getBottom());
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O)
canvas.clipOutRect(tmpRect);
else
canvas.clipRect(tmpRect, Region.Op.DIFFERENCE);
super.onDrawForeground(canvas);
canvas.restore();
}else{
super.onDrawForeground(canvas);
}
}
@Override
public void setForeground(Drawable foreground){
super.setForeground(new PaddedForegroundDrawable(foreground));
}
@Override
public Drawable getForeground(){
if(super.getForeground() instanceof PaddedForegroundDrawable pfd){
return pfd.wrapped;
}
return null;
}
@Override
protected void drawableStateChanged(){
super.drawableStateChanged();
if(label==null || errorState)
return;
ColorStateList color=labelColors==null ? origHintColors : labelColors;
label.setTextColor(color.getColorForState(getDrawableState(), 0xff00ff00));
}
public void setErrorState(CharSequence error){
if(errorState)
return;
errorState=true;
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field_error, getContext().getTheme()));
label.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error));
errorView.setVisibility(VISIBLE);
errorView.setText(error);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
if(errorView.getVisibility()!=GONE){
int width=MeasureSpec.getSize(widthMeasureSpec)-getPaddingLeft()-getPaddingRight();
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
width-=editLP.leftMargin+editLP.rightMargin;
errorView.measure(width | MeasureSpec.EXACTLY, MeasureSpec.UNSPECIFIED);
LayoutParams lp=(LayoutParams) errorView.getLayoutParams();
lp.width=width;
lp.height=errorView.getMeasuredHeight();
lp.gravity=Gravity.LEFT | Gravity.BOTTOM;
lp.leftMargin=editLP.leftMargin;
editLP.bottomMargin=errorView.getMeasuredHeight();
}else{
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
editLP.bottomMargin=0;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private class PaddedForegroundDrawable extends Drawable{
private final Drawable wrapped;
private PaddedForegroundDrawable(Drawable wrapped){
this.wrapped=wrapped;
wrapped.setCallback(new Callback(){
@Override
public void invalidateDrawable(@NonNull Drawable who){
invalidateSelf();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when){
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what){
unscheduleSelf(what);
}
});
}
@Override
public void draw(@NonNull Canvas canvas){
wrapped.draw(canvas);
}
@Override
public void setAlpha(int alpha){
wrapped.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
wrapped.setColorFilter(colorFilter);
}
@Override
public int getOpacity(){
return wrapped.getOpacity();
}
@Override
public boolean setState(@NonNull int[] stateSet){
return wrapped.setState(stateSet);
}
@Override
public int getLayoutDirection(){
return wrapped.getLayoutDirection();
}
@Override
public int getAlpha(){
return wrapped.getAlpha();
}
@Nullable
@Override
public ColorFilter getColorFilter(){
return wrapped.getColorFilter();
}
@Override
public boolean isStateful(){
return wrapped.isStateful();
}
@NonNull
@Override
public int[] getState(){
return wrapped.getState();
}
@NonNull
@Override
public Drawable getCurrent(){
return wrapped.getCurrent();
}
@Override
public void applyTheme(@NonNull Resources.Theme t){
wrapped.applyTheme(t);
}
@Override
public boolean canApplyTheme(){
return wrapped.canApplyTheme();
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
int offset=V.dp(12);
wrapped.setBounds(edit.getLeft()-offset, edit.getTop()-offset, edit.getRight()+offset, edit.getBottom()+offset);
}
}
}

View File

@@ -0,0 +1,58 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
public class NestableScrollView extends ScrollView{
private float downY, touchslop;
private boolean didDisallow;
public NestableScrollView(Context context){
super(context);
init();
}
public NestableScrollView(Context context, AttributeSet attrs){
super(context, attrs);
init();
}
public NestableScrollView(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
init();
}
public NestableScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init(){
touchslop=ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent ev){
if(ev.getAction()==MotionEvent.ACTION_DOWN){
if(canScrollVertically(-1) || canScrollVertically(1)){
getParent().requestDisallowInterceptTouchEvent(true);
didDisallow=true;
}else{
didDisallow=false;
}
downY=ev.getY();
}else if(didDisallow && ev.getAction()==MotionEvent.ACTION_MOVE){
if(Math.abs(downY-ev.getY())>=touchslop){
if(!canScrollVertically((int)(downY-ev.getY()))){
didDisallow=false;
getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
return super.onTouchEvent(ev);
}
}

View File

@@ -0,0 +1,116 @@
package org.joinmastodon.android.utils;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ElevationOnScrollListener extends RecyclerView.OnScrollListener implements View.OnScrollChangeListener{
private boolean isAtTop;
private Animator currentPanelsAnim;
private View[] views;
private FragmentRootLinearLayout fragmentRootLayout;
public ElevationOnScrollListener(FragmentRootLinearLayout fragmentRootLayout, View... views){
isAtTop=true;
this.fragmentRootLayout=fragmentRootLayout;
this.views=views;
for(View v:views){
Drawable bg=v.getBackground().mutate();
v.setBackground(bg);
if(bg instanceof LayerDrawable ld){
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
if(overlay!=null){
overlay.setAlpha(0);
}
}
}
}
public void setViews(View... views){
List<View> oldViews=Arrays.asList(this.views);
this.views=views;
for(View v:views){
if(oldViews.contains(v))
continue;
Drawable bg=v.getBackground().mutate();
v.setBackground(bg);
if(bg instanceof LayerDrawable ld){
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
if(overlay!=null){
overlay.setAlpha(isAtTop ? 0 : 20);
}
}
v.setTranslationZ(isAtTop ? 0 : V.dp(3));
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
handleScroll(recyclerView.getContext(), newAtTop);
}
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
handleScroll(v.getContext(), scrollY<=0);
}
private void handleScroll(Context context, boolean newAtTop){
if(newAtTop!=isAtTop){
isAtTop=newAtTop;
if(currentPanelsAnim!=null)
currentPanelsAnim.cancel();
AnimatorSet set=new AnimatorSet();
ArrayList<Animator> anims=new ArrayList<>();
for(View v:views){
if(v.getBackground() instanceof LayerDrawable ld){
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
if(overlay!=null){
anims.add(ObjectAnimator.ofInt(overlay, "alpha", isAtTop ? 0 : 20));
}
}
anims.add(ObjectAnimator.ofFloat(v, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)));
}
if(fragmentRootLayout!=null){
int color;
if(isAtTop){
color=UiUtils.getThemeColor(context, R.attr.colorM3Background);
}else{
color=UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Background), UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.07843137f);
}
anims.add(ObjectAnimator.ofArgb(fragmentRootLayout, "statusBarColor", color));
}
set.playTogether(anims);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentPanelsAnim=null;
}
});
set.start();
currentPanelsAnim=set;
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Primary" android:state_enabled="true"/>
<item android:color="?colorM3OnSurface" android:alpha="0.38"/>
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSecondaryContainer" android:state_enabled="true"/>
<item android:color="?colorM3OnSurface" android:alpha="0.38"/>
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSecondaryContainer" android:state_selected="true"/>
<item android:color="?colorM3OnSurface"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSecondaryContainer" android:alpha="0.12"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSurfaceVariant" android:alpha="0.12"/>
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Primary" android:state_focused="true"/>
<item android:color="?colorM3OnSurfaceVariant"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Primary" android:alpha="0.12"/>
</selector>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple android:color="@color/m3_primary_overlay" xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="20dp"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true">
<ripple android:color="@color/m3_on_secondary_container_overlay">
<item>
<shape>
<solid android:color="?colorM3SecondaryContainer"/>
<corners android:radius="20dp"/>
</shape>
</item>
</ripple>
</item>
<item>
<shape>
<solid android:color="?colorM3DisabledBackground"/>
<corners android:radius="20dp"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<ripple android:color="@color/m3_on_secondary_container_overlay">
<item>
<shape>
<corners android:radius="8dp"/>
<solid android:color="?colorM3SecondaryContainer"/>
</shape>
</item>
</ripple>
</item>
<item>
<ripple android:color="@color/m3_on_surface_variant_overlay">
<item android:id="@android:id/mask">
<shape>
<corners android:radius="8dp"/>
<solid android:color="#000"/>
</shape>
</item>
<item>
<shape>
<corners android:radius="8dp"/>
<stroke android:color="?colorM3Outline" android:width="1dp"/>
</shape>
</item>
</ripple>
</item>
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#D9000000"/>
<corners android:radius="4dp"/>
</shape>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="12dp" android:top="12dp" android:right="12dp" android:bottom="12dp">
<selector>
<item android:state_focused="true">
<shape>
<stroke android:color="?colorM3Primary" android:width="2dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:color="?colorM3Outline" android:width="1dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
</selector>
</item>
</layer-list>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="12dp" android:top="12dp" android:right="12dp" android:bottom="12dp">
<shape>
<stroke android:color="?colorM3Error" android:width="2dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<selector>
<item android:state_focused="true">
<shape>
<stroke android:color="?colorM3Primary" android:width="2dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:color="?colorM3Outline" android:width="1dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
</selector>
</item>
</layer-list>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="28dp"/>
<solid android:color="?colorM3Surface"/>
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="?colorM3Background"/>
</item>
<item android:id="@+id/color_overlay">
<color android:color="?colorM3Primary"/>
</item>
</layer-list>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="8dp"/>
<solid android:color="#00000000"/>
</shape>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,19V13H5V11H11V5H13V11H19V13H13V19Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@android:color/white"
android:pathData="M29.45,6V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39H39Q39,39 39,39Q39,39 39,39V18.6H42V39Q42,40.2 41.1,41.1Q40.2,42 39,42H9Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6ZM38,6V10.05H42.05V13.05H38V17.1H35V13.05H30.95V10.05H35V6ZM12,33.9H36L28.8,24.3L22.45,32.65L17.75,26.45ZM9,9V14.55V18.6V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="18dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="18dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M7,21Q6.175,21 5.588,20.413Q5,19.825 5,19V6H4V4H9V3H15V4H20V6H19V19Q19,19.825 18.413,20.413Q17.825,21 17,21ZM17,6H7V19Q7,19 7,19Q7,19 7,19H17Q17,19 17,19Q17,19 17,19ZM9,17H11V8H9ZM13,17H15V8H13ZM7,6V19Q7,19 7,19Q7,19 7,19Q7,19 7,19Q7,19 7,19Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,15V13H20V15ZM4,11V9H20V11Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M22,6c0,-1.1 -0.9,-2 -2,-2L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6zM20,6l-8,5 -8,-5h16zM20,18L4,18L4,8l8,5 8,-5v10z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17,7h-4v2h4c1.65,0 3,1.35 3,3s-1.35,3 -3,3h-4v2h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5zM11,15L7,15c-1.65,0 -3,-1.35 -3,-3s1.35,-3 3,-3h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-2zM8,11h8v2L8,13z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M2,17h20v2H2V17zM3.15,12.95L4,11.47l0.85,1.48l1.3,-0.75L5.3,10.72H7v-1.5H5.3l0.85,-1.47L4.85,7L4,8.47L3.15,7l-1.3,0.75L2.7,9.22H1v1.5h1.7L1.85,12.2L3.15,12.95zM9.85,12.2l1.3,0.75L12,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H15v-1.5h-1.7l0.85,-1.47L12.85,7L12,8.47L11.15,7l-1.3,0.75l0.85,1.47H9v1.5h1.7L9.85,12.2zM23,9.22h-1.7l0.85,-1.47L20.85,7L20,8.47L19.15,7l-1.3,0.75l0.85,1.47H17v1.5h1.7l-0.85,1.48l1.3,0.75L20,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H23V9.22z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2m0,10c2.7,0 5.8,1.29 6,2L6,18c0.23,-0.72 3.31,-2 6,-2m0,-12C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="100dp"/>
<solid android:color="#000"/>
</shape>

View File

@@ -1,27 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="257dp"
android:height="67dp"
android:viewportWidth="313"
android:viewportHeight="81">
<path
android:pathData="M72.95,18.45C71.82,9.95 64.5,3.24 55.85,1.95C54.38,1.73 48.85,0.93 36.02,0.93H35.92C23.09,0.93 20.34,1.73 18.87,1.95C10.44,3.22 2.76,9.23 0.88,17.84C-0,22.08 -0.1,26.78 0.07,31.09C0.31,37.27 0.36,43.43 0.91,49.6C1.29,53.69 1.95,57.74 2.9,61.73C4.68,69.11 11.86,75.25 18.9,77.75C26.43,80.35 34.53,80.79 42.29,79C43.14,78.79 43.98,78.56 44.82,78.29C46.71,77.68 48.92,77 50.55,75.81C50.57,75.8 50.59,75.77 50.6,75.75C50.61,75.72 50.62,75.7 50.62,75.66V69.7C50.62,69.7 50.62,69.65 50.6,69.62C50.6,69.6 50.57,69.58 50.55,69.56C50.53,69.55 50.5,69.54 50.48,69.53C50.45,69.53 50.43,69.53 50.41,69.53C45.43,70.73 40.33,71.34 35.23,71.33C26.43,71.33 24.06,67.09 23.39,65.34C22.85,63.82 22.5,62.22 22.36,60.61C22.36,60.59 22.36,60.57 22.37,60.54C22.37,60.52 22.39,60.49 22.42,60.48C22.44,60.47 22.46,60.46 22.49,60.44H22.57C27.46,61.64 32.48,62.25 37.51,62.25C38.72,62.25 39.92,62.25 41.14,62.21C46.19,62.06 51.52,61.81 56.51,60.82C56.63,60.8 56.76,60.77 56.87,60.75C64.72,59.21 72.19,54.42 72.95,42.27C72.97,41.79 73.04,37.25 73.04,36.76C73.04,35.07 73.58,24.79 72.96,18.48L72.95,18.45Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="36.62"
android:startY="0.93"
android:endX="36.62"
android:endY="80.07"
android:type="linear">
<item android:offset="0" android:color="#FF6364FF"/>
<item android:offset="1" android:color="#FF563ACC"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M14.81,23.2C14.81,20.72 16.77,18.72 19.2,18.72C21.62,18.72 23.58,20.73 23.58,23.2C23.58,25.67 21.62,27.68 19.2,27.68C16.77,27.68 14.81,25.67 14.81,23.2Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M80.02,27.06V47.66H72.03V27.67C72.03,23.45 70.3,21.32 66.83,21.32C63,21.32 61.07,23.87 61.07,28.87V39.82H53.14V28.87C53.14,23.84 51.24,21.32 47.38,21.32C43.92,21.32 42.18,23.45 42.18,27.67V47.65H34.21V27.06C34.21,22.86 35.25,19.51 37.35,17.03C39.53,14.54 42.37,13.29 45.89,13.29C49.97,13.29 53.07,14.9 55.11,18.11L57.11,21.52L59.1,18.11C61.14,14.91 64.23,13.29 68.32,13.29C71.84,13.29 74.69,14.55 76.86,17.03C78.96,19.51 80.01,22.83 80.01,27.06H80.02ZM107.49,37.3C109.15,35.51 109.93,33.29 109.93,30.59C109.93,27.89 109.14,25.65 107.49,23.94C105.91,22.15 103.89,21.3 101.45,21.3C99.02,21.3 97.01,22.15 95.41,23.94C93.83,25.65 93.04,27.89 93.04,30.59C93.04,33.29 93.83,35.53 95.41,37.3C97,39 99.02,39.87 101.45,39.87C103.89,39.87 105.9,39.02 107.49,37.3ZM109.93,14.12H117.8V47.06H109.93V43.18C107.55,46.41 104.26,48 99.99,48C95.71,48 92.42,46.36 89.5,43C86.64,39.64 85.18,35.48 85.18,30.61C85.18,25.74 86.65,21.65 89.5,18.29C92.43,14.93 95.92,13.23 99.99,13.23C104.06,13.23 107.55,14.81 109.93,18.02V14.14V14.12ZM144.26,29.97C146.58,31.76 147.73,34.25 147.67,37.41C147.67,40.77 146.52,43.41 144.14,45.24C141.76,47.03 138.89,47.94 135.41,47.94C129.13,47.94 124.87,45.3 122.61,40.11L129.43,35.96C130.34,38.78 132.35,40.25 135.41,40.25C138.22,40.25 139.62,39.33 139.62,37.42C139.62,36.03 137.79,34.78 134.07,33.8C132.66,33.41 131.5,33.01 130.6,32.68C129.31,32.16 128.22,31.56 127.31,30.83C125.05,29.04 123.9,26.68 123.9,23.65C123.9,20.42 124.99,17.85 127.19,16C129.45,14.09 132.19,13.18 135.48,13.18C140.73,13.18 144.56,15.48 147.07,20.16L140.37,24.1C139.4,21.86 137.74,20.74 135.48,20.74C133.11,20.74 131.95,21.65 131.95,23.44C131.95,24.83 133.78,26.08 137.5,27.06C140.37,27.72 142.63,28.7 144.26,29.97H144.27H144.26ZM169.26,22.27H162.37V35.98C162.37,37.63 162.98,38.63 164.15,39.08C165,39.4 166.71,39.47 169.27,39.34V47.05C163.98,47.71 160.14,47.17 157.88,45.41C155.62,43.7 154.53,40.53 154.53,36V22.27H149.23V14.1H154.53V7.46L162.39,4.89V14.12H169.29V22.29H169.27L169.26,22.27ZM194.34,37.1C195.92,35.4 196.71,33.22 196.71,30.58C196.71,27.94 195.92,25.78 194.34,24.05C192.74,22.35 190.79,21.48 188.42,21.48C186.04,21.48 184.09,22.33 182.49,24.05C180.97,25.84 180.18,28 180.18,30.58C180.18,33.16 180.97,35.31 182.49,37.1C184.08,38.81 186.04,39.67 188.42,39.67C190.79,39.67 192.74,38.82 194.34,37.1ZM176.96,42.96C173.85,39.6 172.32,35.52 172.32,30.58C172.32,25.63 173.85,21.62 176.96,18.26C180.07,14.9 183.91,13.19 188.42,13.19C192.92,13.19 196.77,14.9 199.87,18.26C202.97,21.62 204.57,25.77 204.57,30.58C204.57,35.39 202.97,39.6 199.87,42.96C196.76,46.32 192.98,47.96 188.42,47.96C183.85,47.96 180.06,46.32 176.96,42.96ZM230.86,37.29C232.45,35.5 233.24,33.28 233.24,30.58C233.24,27.87 232.45,25.63 230.86,23.93C229.28,22.14 227.26,21.29 224.82,21.29C222.39,21.29 220.37,22.14 218.73,23.93C217.14,25.63 216.35,27.87 216.35,30.58C216.35,33.28 217.14,35.52 218.73,37.29C220.38,38.99 222.45,39.86 224.82,39.86C227.2,39.86 229.27,39 230.86,37.29ZM233.24,0.92H241.11V47.05H233.24V43.17C230.93,46.39 227.63,47.99 223.36,47.99C219.09,47.99 215.75,46.35 212.8,42.98C209.93,39.62 208.48,35.47 208.48,30.6C208.48,25.73 209.95,21.64 212.8,18.28C215.72,14.92 219.26,13.22 223.36,13.22C227.45,13.22 230.93,14.8 233.24,18.01V0.93V0.92ZM268.74,37.07C270.32,35.36 271.12,33.18 271.12,30.54C271.12,27.9 270.32,25.74 268.74,24.01C267.15,22.31 265.21,21.45 262.82,21.45C260.43,21.45 258.5,22.3 256.9,24.01C255.37,25.8 254.58,27.96 254.58,30.54C254.58,33.12 255.37,35.28 256.9,37.07C258.48,38.77 260.44,39.64 262.82,39.64C265.2,39.64 267.14,38.78 268.74,37.07ZM251.36,42.92C248.26,39.56 246.73,35.48 246.73,30.54C246.73,25.6 248.25,21.58 251.36,18.22C254.47,14.86 258.32,13.15 262.82,13.15C267.32,13.15 271.18,14.86 274.27,18.22C277.38,21.58 278.97,25.73 278.97,30.54C278.97,35.35 277.38,39.56 274.27,42.92C271.16,46.28 267.38,47.93 262.82,47.93C258.26,47.93 254.46,46.28 251.36,42.92ZM313,26.78V47.01H305.14V27.84C305.14,25.66 304.59,24.01 303.48,22.77C302.45,21.65 300.98,21.07 299.09,21.07C294.65,21.07 292.39,23.77 292.39,29.24V47.03H284.53V14.1H292.39V17.81C294.28,14.71 297.28,13.19 301.47,13.19C304.82,13.19 307.57,14.37 309.71,16.81C311.91,19.24 313,22.54 313,26.82"
android:fillColor="#ffffff"/>
</vector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#fff"/>
</shape>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@@ -10,4 +11,62 @@
android:layout_gravity="center"
android:scaleType="centerCrop"/>
<!-- This is hidden from screenreaders because that same alt text is set as content description on the ImageView -->
<FrameLayout
android:id="@+id/alt_text_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:importantForAccessibility="noHideDescendants"
android:background="@drawable/bg_image_alt_overlay">
<TextView
android:id="@+id/alt_button"
android:layout_width="40dp"
android:layout_height="22dp"
android:textAppearance="@style/m3_label_large"
android:textColor="#FFF"
android:gravity="center"
android:includeFontPadding="false"
android:text="ALT"/>
<ImageButton
android:id="@+id/alt_text_close"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|top"
android:src="@drawable/ic_baseline_close_24"
android:tint="#FFF"
android:background="?android:selectableItemBackgroundBorderless"/>
<org.joinmastodon.android.ui.views.NestableScrollView
android:id="@+id/alt_text_scroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="40dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/alt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="#FFF"
tools:text="Alt text goes here"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.NestableScrollView>
</FrameLayout>
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout>

View File

@@ -165,16 +165,16 @@
android:background="?colorBackgroundLightest"
android:elevation="2dp"
android:outlineProvider="bounds"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingHorizontal="10dp"
android:layout_marginEnd="6dp"
android:layoutDirection="locale">
<ImageButton
android:id="@+id/btn_media"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:background="?android:attr/actionBarItemBackground"
android:padding="0px"
android:tint="@color/compose_button"
android:tintMode="src_in"
@@ -184,10 +184,10 @@
<ImageButton
android:id="@+id/btn_poll"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:background="?android:attr/actionBarItemBackground"
android:padding="0px"
android:tint="@color/compose_button"
android:tintMode="src_in"
@@ -197,10 +197,10 @@
<ImageButton
android:id="@+id/btn_emoji"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:background="?android:attr/actionBarItemBackground"
android:padding="0px"
android:tint="@color/compose_button"
android:tintMode="src_in"
@@ -210,10 +210,10 @@
<ImageButton
android:id="@+id/btn_spoiler"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:background="?android:attr/actionBarItemBackground"
android:padding="0px"
android:tint="@color/compose_button"
android:tintMode="src_in"
@@ -223,10 +223,10 @@
<ImageButton
android:id="@+id/btn_visibility"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:background="?android:attr/actionBarItemBackground"
android:padding="0px"
android:tint="@color/compose_button"
android:tintMode="src_in"

View File

@@ -1,15 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context=".fragments.onboarding.AccountActivationFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?colorBackgroundLight">
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
@@ -17,40 +18,78 @@
android:orientation="vertical"
android:clipChildren="false">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:layout_marginTop="32dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="12dp"
android:gravity="center_vertical"
android:text="@string/confirm_email_title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textAppearance="@style/m3_title_medium"
android:textColor="?android:textColorSecondary"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="12dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:text="@string/confirm_email_subtitle"/>
<Space
android:layout_width="1dp"
android:layout_height="0px"
android:layout_weight="1"/>
<ImageView
android:layout_width="wrap_content"
android:layout_width="230dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="32dp"
android:layout_marginTop="12dp"
android:importantForAccessibility="no"
android:adjustViewBounds="true"
android:src="@drawable/confirm_email_art"/>
<Space
android:layout_width="1dp"
android:layout_height="0px"
android:layout_weight="1"/>
</LinearLayout>
</ScrollView>
<include layout="@layout/button_bar_activation" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_horizontal">
</me.grishka.appkit.views.FragmentRootLinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/confirm_email_didnt_get"/>
<Button
android:id="@+id/btn_resend"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginStart="36dp"
android:text="@string/resend"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:fontFeatureSettings="'tnum'"
style="@style/Widget.Mastodon.M3.Button.Filled"/>
</LinearLayout>
<Button
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/open_email_app" />
</LinearLayout>

View File

@@ -5,14 +5,103 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/appkit_loader_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?colorBackgroundLight">
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:background="@drawable/bg_onboarding_panel">
<EditText
android:id="@+id/search_edit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:elevation="0dp"
android:inputType="textFilter|textUri"
android:layout_toEndOf="@+id/btn_back"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/m3_body_large"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:paddingStart="12dp"
android:paddingEnd="40dp"
android:drawableStart="@drawable/ic_m3_search"
android:drawablePadding="8dp"
android:drawableTint="?colorM3OnSurfaceVariant"
android:gravity="center_vertical"
android:textColorHint="?colorM3OnSurfaceVariant"
android:textColor="?colorM3OnSurfaceVariant"
android:background="@drawable/round_rect"
android:backgroundTint="?colorM3SurfaceVariant"
android:hint="@string/search_communities"/>
<View
android:id="@+id/focus_thing"
android:layout_width="1dp"
android:layout_height="1dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:importantForAccessibility="no"/>
<ImageButton
android:id="@+id/btn_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="20dp"
android:layout_marginStart="8dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:background="?android:selectableItemBackgroundBorderless"
android:tint="?colorM3OnSurface"
android:contentDescription="@string/back"
android:src="@drawable/ic_arrow_back"/>
<ImageButton
android:id="@+id/clear"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?android:selectableItemBackgroundBorderless"
android:layout_alignEnd="@id/search_edit"
android:layout_alignTop="@id/search_edit"
android:layout_margin="12dp"
android:contentDescription="@string/clear"
android:tint="?colorM3OnSurfaceVariant"
android:visibility="gone"
android:src="@drawable/ic_m3_cancel"/>
<HorizontalScrollView
android:id="@+id/filters_scroll"
android:layout_below="@id/search_edit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/filters_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:showDividers="middle"
android:divider="@drawable/empty_8dp">
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
<FrameLayout
android:id="@+id/appkit_loader_content"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1">
android:layout_weight="1"
android:background="?colorM3Surface">
<include layout="@layout/loading"
android:id="@+id/loading"/>
@@ -33,32 +122,39 @@
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLight"
android:outlineProvider="bounds"
android:orientation="horizontal"
android:elevation="3dp">
android:orientation="vertical"
android:background="@drawable/bg_onboarding_panel">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/signup_random_server_explain"/>
<Button
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:id="@+id/btn_random_instance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="145dp"
style="?secondaryLargeButtonStyle"
android:text="@string/back"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
style="@style/Widget.Mastodon.M3.Button.Text"
android:text="@string/pick_server_for_me"/>
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="?primaryLargeButtonStyle"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/next" />
</LinearLayout>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/appkit_loader_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?android:colorBackground">
<include layout="@layout/appkit_toolbar"/>
<FrameLayout
android:id="@+id/appkit_loader_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<include layout="@layout/loading"
android:id="@+id/loading"/>
<ViewStub android:layout="?errorViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/error"
android:visibility="gone"/>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/content_stub"/>
</FrameLayout>
<LinearLayout
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:background="@drawable/bg_onboarding_panel">
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
style="@style/Widget.Mastodon.M3.Button.Tonal"
android:text="@string/follow_all"/>
<Button
android:id="@+id/btn_skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/skip"/>
</LinearLayout>
</me.grishka.appkit.views.FragmentRootLinearLayout>

View File

@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroller"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:text="@string/profile_setup_subtitle"/>
<ImageView
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="144dp"
android:foreground="@drawable/ic_add_photo_alternate_48px"
android:foregroundGravity="center"
android:foregroundTint="?colorM3OnSecondaryContainer"
android:scaleType="centerCrop"
android:background="?colorM3SecondaryContainer"/>
<FrameLayout
android:layout_width="104dp"
android:layout_height="104dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="-44dp"
android:background="@drawable/bg_onboarding_avatar">
<ImageView
android:id="@+id/avatar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:foreground="@drawable/ic_add_photo_alternate_48px"
android:foregroundGravity="center"
android:foregroundTint="?colorM3OnSecondaryContainer"
android:scaleType="centerCrop"
android:background="?colorM3SecondaryContainer"/>
</FrameLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/display_name_wrap"
android:layout_width="match_parent"
android:layout_height="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/display_name"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textPersonName|textCapWords"
android:autofillHints="name"
android:singleLine="true"
android:hint="@string/display_name"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/bio_wrap"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="-8dp"
android:layout_marginBottom="4dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/bio"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textMultiLine|textCapSentences"
android:hint="@string/profile_bio"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<org.joinmastodon.android.ui.views.ReorderableLinearLayout
android:id="@+id/profile_fields"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
<Button
android:id="@+id/add_row"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:drawableEnd="@drawable/ic_add_24px"
android:drawableTint="?colorM3OnSurface"
android:drawablePadding="16dp"
android:singleLine="true"
android:ellipsize="end"
android:background="?android:attr/selectableItemBackground"
android:stateListAnimator="@null"
android:text="@string/profile_add_row"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/profile_setup_explanation"/>
</LinearLayout>
</ScrollView>
<LinearLayout
android:background="@drawable/bg_onboarding_panel"
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Button
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/next" />
</LinearLayout>
</LinearLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -8,41 +8,37 @@
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?colorBackgroundLight"/>
android:layout_weight="1"/>
<LinearLayout
android:background="@drawable/bg_onboarding_panel"
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLight"
android:outlineProvider="bounds"
android:orientation="horizontal"
android:elevation="3dp">
android:orientation="vertical">
<Button
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="145dp"
style="?secondaryLargeButtonStyle"
android:text="@string/back"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
style="@style/Widget.Mastodon.M3.Button.Text"
android:text="@string/server_rules_disagree"/>
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="?primaryLargeButtonStyle"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/i_agree" />
</LinearLayout>
</me.grishka.appkit.views.FragmentRootLinearLayout>
</LinearLayout>

View File

@@ -1,15 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroller"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?colorBackgroundLight">
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
@@ -17,68 +18,59 @@
android:orientation="vertical"
android:clipChildren="false">
<TextView
android:id="@+id/title"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/display_name_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:layout_marginTop="32dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:gravity="center_vertical"
tools:text="@string/signup_title"/>
android:minHeight="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<FrameLayout
android:id="@+id/ava_wrap"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp">
<ImageView
android:id="@+id/avatar"
<EditText
android:id="@+id/display_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/default_avatar"/>
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textPersonName|textCapWords"
android:autofillHints="name"
android:singleLine="true"
android:hint="@string/display_name"/>
<TextView
android:layout_width="match_parent"
android:layout_height="22dp"
android:layout_gravity="bottom"
android:gravity="center"
android:background="@color/gray_800t"
android:textAppearance="@style/m3_label_large"
android:textColor="#eee"
android:text="@string/edit_photo"/>
<View
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="start|top"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:backgroundTint="?colorM3OnSurfaceVariant"
android:background="@drawable/ic_outline_person_24"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<EditText
android:id="@+id/display_name"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/username_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:inputType="textPersonName|textCapWords"
android:autofillHints="name"
android:singleLine="true"
android:hint="@string/display_name"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="24dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
android:layout_height="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textFilter|textNoSuggestions"
android:autofillHints="username"
android:singleLine="true"
@@ -88,71 +80,165 @@
<TextView
android:id="@+id/domain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="5dp"
android:layout_gravity="right|center_vertical"
android:layout_height="56dp"
android:layout_gravity="right|top"
android:layout_marginRight="20dp"
android:layout_marginTop="8dp"
android:paddingLeft="8dp"
android:paddingRight="16dp"
android:textAppearance="@style/m3_title_medium"
android:textAppearance="@style/m3_body_large"
android:gravity="center_vertical"
tools:text="\@mastodon.social"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<EditText
android:id="@+id/email"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/email_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="8dp"
android:inputType="textEmailAddress"
android:autofillHints="emailAddress"
android:singleLine="true"
android:hint="@string/email"/>
android:paddingTop="4dp"
android:paddingBottom="12dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/password"
<EditText
android:id="@+id/email"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textEmailAddress"
android:autofillHints="emailAddress"
android:singleLine="true"
android:hint="@string/email"/>
<View
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="start|top"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:backgroundTint="?colorM3OnSurfaceVariant"
android:background="@drawable/ic_outline_email_24"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/password_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:inputType="textPassword"
android:autofillHints="password"
android:singleLine="true"
android:fontFamily="sans-serif"
android:hint="@string/password"/>
android:paddingTop="4dp"
android:paddingBottom="12dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textPassword"
android:autofillHints="password"
android:singleLine="true"
android:fontFamily="sans-serif"
android:hint="@string/password"/>
<View
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="start|top"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:backgroundTint="?colorM3OnSurfaceVariant"
android:background="@drawable/ic_outline_password_24"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/password_confirm_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:paddingBottom="12dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/password_confirm"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textPassword"
android:autofillHints="password"
android:singleLine="true"
android:fontFamily="sans-serif"
android:hint="@string/confirm_password"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:layout_marginStart="56dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="-8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/password_note"/>
<EditText
android:id="@+id/reason"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/reason_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:inputType="textCapSentences|textMultiLine"
android:hint="@string/signup_reason"/>
android:paddingTop="4dp"
android:paddingBottom="12dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/reason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="12dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textCapSentences|textMultiLine"
android:hint="@string/signup_reason"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<TextView
android:id="@+id/reason_explain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:layout_marginStart="56dp"
android:layout_marginEnd="20dp"
android:layout_marginTop="-8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/signup_reason_note"/>
</LinearLayout>
@@ -163,32 +249,19 @@
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLight"
android:outlineProvider="bounds"
android:orientation="horizontal"
android:elevation="3dp">
<Button
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="145dp"
style="?secondaryLargeButtonStyle"
android:text="@string/back"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
android:background="@drawable/bg_onboarding_panel">
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="?primaryLargeButtonStyle"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/next" />
</LinearLayout>

View File

@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.SizeListenerFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<org.joinmastodon.android.ui.views.SizeListenerFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:clipToPadding="false"
android:clipChildren="false">
android:clipToPadding="false">
<View
android:id="@+id/blue_fill"
@@ -18,51 +17,60 @@
<FrameLayout
android:id="@+id/art_container"
android:layout_width="450dp"
android:layout_height="631dp"
android:layout_marginTop="40dp"
android:layout_gravity="center">
android:layout_width="360dp"
android:layout_height="640dp"
android:layout_gravity="center"
tools:ignore="rtlHardcoded">
<ImageView
android:id="@+id/art_clouds"
android:layout_width="450dp"
android:layout_height="589dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_width="414dp"
android:layout_height="541dp"
android:layout_marginTop="91dp"
android:layout_gravity="top|left"
android:alpha="0.3"
android:importantForAccessibility="no"
android:src="@drawable/splash_art_layer0"/>
<ImageView
android:id="@+id/art_plane_elephant"
android:layout_width="245.64dp"
android:layout_height="72.65dp"
android:layout_marginLeft="-101.55dp"
android:layout_marginTop="238.12dp"
android:layout_gravity="left|top"
android:alpha="0.3"
android:importantForAccessibility="no"
android:src="@drawable/splash_art_layer4"/>
<ImageView
android:id="@+id/art_right_hill"
android:layout_width="218dp"
android:layout_height="255dp"
android:layout_gravity="bottom|right"
android:layout_marginBottom="156dp"
android:layout_marginRight="11dp"
android:layout_width="150.84dp"
android:layout_height="176.44dp"
android:layout_gravity="top|left"
android:layout_marginLeft="322dp"
android:layout_marginTop="310dp"
android:importantForAccessibility="no"
android:src="@drawable/splash_art_layer1"/>
<ImageView
android:id="@+id/art_left_hill"
android:layout_width="285dp"
android:layout_height="222dp"
android:layout_gravity="bottom|left"
android:layout_marginLeft="-6dp"
android:layout_marginBottom="243dp"
android:layout_width="197.2dp"
android:layout_height="153.61dp"
android:layout_gravity="top|left"
android:layout_marginTop="294dp"
android:importantForAccessibility="no"
android:src="@drawable/splash_art_layer2"/>
<ImageView
android:id="@+id/art_center_hill"
android:layout_width="457dp"
android:layout_height="397dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="51dp"
android:layout_width="400dp"
android:layout_height="346dp"
android:layout_gravity="top|left"
android:layout_marginTop="294dp"
android:importantForAccessibility="no"
android:src="@drawable/splash_art_layer3"/>
<ImageView
android:id="@+id/art_plane_elephant"
android:layout_width="355dp"
android:layout_height="105dp"
android:layout_gravity="left|top"
android:src="@drawable/splash_art_layer4"/>
</FrameLayout>
<View
@@ -73,36 +81,65 @@
android:transformPivotY="1px"
android:background="#478E6A"/>
<org.joinmastodon.android.ui.views.SplashLogoView
android:layout_width="261dp"
android:layout_height="71dp"
android:layout_gravity="center_horizontal|top"
android:layout_marginTop="24dp"
android:scaleType="center"
android:importantForAccessibility="no"
android:src="@drawable/splash_logo"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:padding="16dp"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical">
<Button
android:id="@+id/btn_get_started"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:id="@+id/pager_dots"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="13dp"
style="@style/Widget.Mastodon.Button.Large.Primary_LightOnDark"
android:text="@string/get_started"/>
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:orientation="horizontal">
<View
android:layout_width="8dp"
android:layout_height="8dp"
android:background="@drawable/white_circle"/>
<View
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:alpha="0.3"
android:background="@drawable/white_circle"/>
<View
android:layout_width="8dp"
android:layout_height="8dp"
android:alpha="0.3"
android:background="@drawable/white_circle"/>
</LinearLayout>
<Button
android:id="@+id/btn_log_in"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.Mastodon.Button.Large.Primary_DarkOnLight"
android:background="@drawable/bg_button_green"
android:text="@string/log_in"/>
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
style="@style/Widget.Mastodon.M3.Button.Text"
android:textColor="#fff"
android:text="@string/already_have_account"/>
<Button
android:id="@+id/btn_get_started"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/get_started"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.SizeListenerFrameLayout>

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_marginTop="32dp"
android:layout_marginLeft="19dp"
android:layout_marginRight="19dp"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:gravity="center_vertical"
android:text="@string/instance_catalog_title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="19dp"
android:layout_marginRight="19dp"
android:layout_marginBottom="24dp"
android:textAppearance="@style/m3_title_medium"
android:textColor="?android:textColorSecondary"
android:text="@string/instance_catalog_subtitle"/>
<org.joinmastodon.android.ui.tabs.TabLayout
android:id="@+id/categories_list"
android:layout_width="match_parent"
android:layout_height="72dp"
android:background="@drawable/bg_catalog_tabs"
app:tabIndicator="@drawable/mtrl_tabs_default_indicator"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="?android:textColorPrimary"
app:tabMinWidth="120dp"
app:tabMaxWidth="120dp"
app:tabMode="scrollable"/>
<EditText
android:id="@+id/search_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textFilter|textNoSuggestions"
android:singleLine="true"
android:imeOptions="actionGo"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="19dp"
android:layout_marginBottom="3dp"
android:drawableStart="@drawable/ic_fluent_search_20_regular"
android:drawablePadding="8dp"
android:drawableTint="?android:textColorSecondary"
android:hint="@string/search_communities"/>
</LinearLayout>

View File

@@ -64,6 +64,7 @@
android:contentDescription="@string/clear"
android:stateListAnimator="@null"
android:elevation="0dp"
android:tint="?colorM3OnSurfaceVariant"
android:src="@drawable/ic_m3_cancel"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>

View File

@@ -3,17 +3,20 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<RadioButton
android:id="@+id/radiobtn"
android:layout_width="24dp"
android:layout_width="28dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:layout_centerVertical="true"
android:layout_marginEnd="14dp"
android:layout_marginStart="-3dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:button="@drawable/ic_round_checkbox"
android:buttonTint="?android:textColorSecondary"
android:buttonTint="@color/m3_radiobutton_tint"
android:background="@null"
android:focusable="false"
android:clickable="false"/>
@@ -24,13 +27,13 @@
android:layout_height="wrap_content"
android:layout_toEndOf="@id/radiobtn"
android:layout_alignParentTop="true"
android:layout_marginBottom="4dp"
android:textAppearance="@style/m3_title_medium"
android:textAppearance="@style/m3_body_large"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textSize="16sp"
android:minHeight="24dp"
android:textColor="?colorM3OnSurface"
tools:text="mastodon.social"/>
<TextView
@@ -41,35 +44,9 @@
android:layout_below="@id/title"
android:layout_marginBottom="8dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:textColor="?colorM3OnSurfaceVariant"
android:textSize="14sp"
android:lineSpacingExtra="4sp"
tools:text="General-purpose server run by the lead developer of Mastodon"/>
<TextView
android:id="@+id/user_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/radiobtn"
android:layout_below="@id/description"
android:textAppearance="@style/m3_label_medium"
android:textColor="?android:textColorSecondary"
android:drawableStart="@drawable/ic_fluent_people_community_16_regular"
android:drawableTint="?android:textColorSecondary"
android:drawablePadding="8dp"
tools:text="588.8K"/>
<TextView
android:id="@+id/lang"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/user_count"
android:layout_below="@id/description"
android:layout_marginStart="24dp"
android:textAppearance="@style/m3_label_medium"
android:textColor="?android:textColorSecondary"
android:drawableStart="@drawable/ic_fluent_local_language_16_regular"
android:drawableTint="?android:textColorSecondary"
android:drawablePadding="8dp"
tools:text="EN"/>
</RelativeLayout>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:paddingStart="56dp"
android:paddingTop="12dp"
android:paddingEnd="24dp"
android:paddingBottom="12dp">
</TextView>

View File

@@ -3,42 +3,37 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:elevation="3dp"
android:background="?colorBackgroundLightest"
android:foreground="?android:selectableItemBackground">
<ImageView
android:id="@+id/favicon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginTop="2dp"
android:importantForAccessibility="no"
tools:src="#0f0"/>
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:paddingStart="16dp"
android:paddingEnd="24dp">
<TextView
android:id="@+id/domain"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/favicon"
android:layout_marginStart="4dp"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_title_small"
android:textColor="?android:textColorPrimary"
android:gravity="center_vertical"
tools:text="joinmastodon.org"/>
<View
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:backgroundTint="?colorM3Primary"
android:background="@drawable/ic_outline_link_24"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_below="@id/domain"
android:layout_marginTop="6dp"
android:textAppearance="@style/m3_title_medium"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="Mastodon for Android privacy policy"/>
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icon"
android:layout_marginStart="16dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3Primary"
tools:text="Privacy Policy - example.social"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icon"
android:layout_below="@id/title"
android:layout_marginStart="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="Privacy Policy - example.social"/>
</RelativeLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingEnd="24dp"
android:paddingBottom="12dp"
android:paddingStart="16dp"
android:baselineAligned="false">
<TextView
android:id="@+id/number"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:textColor="?colorM3Primary"
android:fontFamily="sans-serif-condensed"
android:textStyle="bold"
android:textSize="22dp"
android:gravity="center"
android:includeFontPadding="false"
tools:text="1"/>
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:textAppearance="@style/m3_body_large"
tools:text="No discrimination, including (but not limited to) racism, sexism, homophobia or transphobia."/>
</LinearLayout>

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<FrameLayout
android:id="@+id/action_btn_wrap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="16dp"
android:layout_marginTop="18dp"
android:layout_marginStart="-8dp">
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/action_btn"
android:layout_width="wrap_content"
android:layout_height="36dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
tools:text="Follow"/>
<ProgressBar
android:id="@+id/action_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
style="?android:progressBarStyleSmall"
android:elevation="10dp"
android:outlineProvider="none"
android:indeterminateTint="?colorM3OnPrimary"
android:visibility="gone"/>
</FrameLayout>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_toEndOf="@id/avatar"
android:layout_toStartOf="@id/action_btn_wrap"
android:layout_alignParentTop="true"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_title_medium"
android:textColor="?colorM3OnSurface"
tools:text="User Name"/>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/avatar"
android:layout_below="@id/name"
android:layout_toStartOf="@id/action_btn_wrap"
android:layout_marginEnd="16dp"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textAppearance="@style/m3_title_small"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="\@username@server.social"/>
<TextView
android:id="@+id/bio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/avatar"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="12dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurface"
tools:text="Description"/>
</RelativeLayout>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="16dp"
android:clipToPadding="false"
android:background="?colorM3Background">
<ImageView
android:id="@+id/dragger_thingy"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentStart="true"
android:layout_marginEnd="8dp"
android:scaleType="center"
android:tint="?colorM3OnSurface"
android:contentDescription="@string/reorder"
android:src="@drawable/ic_drag_handle_24px"/>
<ImageButton
android:id="@+id/delete"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:scaleType="center"
android:tint="?colorM3OnSurface"
android:background="?android:actionBarItemBackground"
android:contentDescription="@string/delete"
android:src="@drawable/ic_delete_24px"/>
<EditText
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_toEndOf="@id/dragger_thingy"
android:layout_toStartOf="@id/delete"
style="@style/Widget.Mastodon.M3.EditText"
android:inputType="textCapSentences"
android:hint="@string/field_content"
android:saveEnabled="false"
android:singleLine="true"/>
<EditText
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_toEndOf="@id/dragger_thingy"
android:layout_toStartOf="@id/delete"
android:layout_below="@id/content"
style="@style/Widget.Mastodon.M3.EditText"
android:inputType="textCapSentences"
android:hint="@string/field_label"
android:saveEnabled="false"
android:singleLine="true"/>
</RelativeLayout>

View File

@@ -37,6 +37,10 @@
<attr name="colorM3Outline" format="color"/>
<attr name="colorM3DisabledBackground" format="color"/>
<attr name="colorM3PressedOverlay" format="color"/>
<attr name="colorM3Error" format="color"/>
<attr name="colorM3OnError" format="color"/>
<attr name="colorM3ErrorContainer" format="color"/>
<attr name="colorM3OnErrorContainer" format="color"/>
<attr name="primaryLargeButtonStyle" format="reference"/>
<attr name="secondaryLargeButtonStyle" format="reference"/>
@@ -48,5 +52,6 @@
<declare-styleable name="FloatingHintEditTextLayout">
<attr name="editTextOffsetY" format="dimension"/>
<attr name="android:labelTextSize" format="dimension"/>
<attr name="labelTextColor" format="color"/>
</declare-styleable>
</resources>

View File

@@ -2,7 +2,8 @@
<resources>
<string name="app_name" translatable="false">Mastodon</string>
<string name="get_started">Get started</string>
<string name="get_started">Create account</string>
<string name="already_have_account">I already have an account</string>
<string name="log_in">Log in</string>
<string name="next">Next</string>
<string name="loading_instance">Retrieving server info…</string>
@@ -178,15 +179,16 @@
<string name="back">Back</string>
<string name="instance_catalog_title">Mastodon is made of users on different servers.</string>
<string name="instance_catalog_subtitle">Pick a server based on your interests, region, or a general purpose one. You can still connect with everyone, regardless of server.</string>
<string name="search_communities">Search servers or enter URL</string>
<string name="instance_rules_title">Some ground rules</string>
<string name="instance_rules_subtitle">Take a minute to review the rules set and enforced by %s admins.</string>
<string name="signup_title">Let\'s get you set up on %s</string>
<string name="search_communities">Server name or URL</string>
<string name="instance_rules_title">Server Rules</string>
<string name="instance_rules_subtitle">By continuing, you agree to follow by the following rules set and enforced by the %s moderators.</string>
<string name="signup_title">Create Account</string>
<string name="edit_photo">edit</string>
<string name="display_name">display name</string>
<string name="username">username</string>
<string name="email">email</string>
<string name="password">password</string>
<string name="display_name">Name</string>
<string name="username">Username</string>
<string name="email">Email</string>
<string name="password">Password</string>
<string name="confirm_password">Confirm password</string>
<string name="password_note">Include capital letters, special characters, and numbers to increase your password strength.</string>
<string name="category_academia">Academia</string>
<string name="category_activism">Activism</string>
@@ -201,8 +203,10 @@
<string name="category_music">Music</string>
<string name="category_regional">Regional</string>
<string name="category_tech">Tech</string>
<string name="confirm_email_title">One last thing</string>
<string name="confirm_email_subtitle">Tap the link we emailed to you to verify your account.</string>
<string name="confirm_email_title">Check Your Inbox</string>
<!-- %s is the email address -->
<string name="confirm_email_subtitle">Tap the link we sent you to verify %s. We\'ll wait right here.</string>
<string name="confirm_email_didnt_get">Didn\'t get a link?</string>
<string name="resend">Resend</string>
<string name="open_email_app">Open email app</string>
<string name="resent_email">Confirmation email sent</string>
@@ -290,7 +294,7 @@
<string name="open_in_browser">Open in browser</string>
<string name="hide_boosts_from_user">Hide reblogs from %s</string>
<string name="show_boosts_from_user">Show reblogs from %s</string>
<string name="signup_reason">why do you want to join?</string>
<string name="signup_reason">Why do you want to join?</string>
<string name="signup_reason_note">This will help us review your application.</string>
<string name="clear">Clear</string>
<string name="profile_header">Header image</string>
@@ -384,8 +388,8 @@
<!-- %s is file size -->
<string name="download_update">Download (%s)</string>
<string name="install_update">Install</string>
<string name="privacy_policy_title">Mastodon and your privacy</string>
<string name="privacy_policy_subtitle">Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your server\'s privacy policy.</string>
<string name="privacy_policy_title">Your Privacy</string>
<string name="privacy_policy_subtitle">Although the Mastodon app does not collect any data, the server you sign up through may have a different policy.\n\nIf you disagree with the policy for %s, you can go back and pick a different server.</string>
<string name="i_agree">I Agree</string>
<string name="empty_list">This list is empty</string>
<string name="instance_signup_closed">This server does not accept new registrations.</string>
@@ -397,4 +401,41 @@
<string name="login_title">Welcome Back</string>
<string name="login_subtitle">Log in with the server where you created your account.</string>
<string name="server_url">Server URL</string>
<string name="welcome_page1_title">What is Mastodon?</string>
<string name="welcome_page1_text">Imagine you have an email address that ends with @example.com.\n\nYou can still send and receive emails from anyone, even if their email ends in @gmail.com or @icloud.com or @example.com.</string>
<string name="welcome_page2_title">Mastodon is like that.</string>
<string name="welcome_page2_text">Your handle might be @gothgirl654@example.social, but you can still follow, reblog, and chat with @fallout5ever@example.online.</string>
<string name="welcome_page3_title">How do I pick a server?</string>
<string name="welcome_page3_text">Different people choose different servers for any number of reasons. art.example is a great place for artists, while glasgow.example might be a good pick for Scots.\n\nYou cant go wrong with any of our recommend servers, so regardless of which one you pick (or if you enter your own in the server search bar), youll never miss a beat anywhere.</string>
<string name="signup_random_server_explain">We\'ll pick a server based on your language if you continue without making a selection.</string>
<string name="server_filter_any_language">Any Language</string>
<string name="server_filter_instant_signup">Instant Sign-up</string>
<string name="server_filter_manual_review">Manual Review</string>
<string name="server_filter_any_signup_speed">Any Sign-up Speed</string>
<string name="server_filter_region_europe">Europe</string>
<string name="server_filter_region_north_america">North America</string>
<string name="server_filter_region_south_america">South America</string>
<string name="server_filter_region_africa">Africa</string>
<string name="server_filter_region_asia">Asia</string>
<string name="server_filter_region_oceania">Oceania</string>
<string name="not_accepting_new_members">Not accepting new members</string>
<string name="category_special_interests">Special Interests</string>
<string name="signup_passwords_dont_match">Passwords don\'t match</string>
<string name="pick_server_for_me">Pick for me</string>
<string name="profile_add_row">Add row</string>
<string name="profile_setup">Profile setup</string>
<string name="profile_setup_subtitle">You can always complete this later in the Profile tab.</string>
<string name="profile_setup_explanation">You can add up to four profile fields for anything you want. Location, links, pronouns — the sky is the limit.</string>
<string name="popular_on_mastodon">Popular on Mastodon</string>
<string name="follow_all">Follow all</string>
<string name="server_rules_disagree">Disagree</string>
<string name="privacy_policy_explanation">TL;DR: We don\'t collect or process anything.</string>
<!-- %s is server domain -->
<string name="server_policy_disagree">Disagree with %s</string>
<string name="profile_bio">Bio</string>
<!-- Shown in a progress dialog when you tap "follow all" -->
<string name="sending_follows">Following users…</string>
<!-- %1$s is server domain, %2$s is email domain. You can reorder these placeholders to fit your language better. -->
<string name="signup_email_domain_blocked">%1$s doesn\'t allow signups from %2$s. Try a different one or &lt;a>pick a different server&lt;/a>.</string>
<string name="signup_username_taken">This username is taken.</string>
</resources>

View File

@@ -66,6 +66,10 @@
<item name="colorM3Outline">@color/m3_sys_light_outline</item>
<item name="colorM3DisabledBackground">#1F1F1F1F</item>
<item name="colorM3PressedOverlay">@color/m3_sys_light_on_primary</item>
<item name="colorM3Error">#B3261E</item>
<item name="colorM3OnError">#FFF</item>
<item name="colorM3ErrorContainer">#F9DEDC</item>
<item name="colorM3OnErrorContainer">#410E0B</item>
</style>
<style name="Theme.Mastodon.Dark" parent="Theme.AppKit">
@@ -136,6 +140,10 @@
<item name="colorM3Outline">@color/m3_sys_dark_outline</item>
<item name="colorM3DisabledBackground">#1FE3E3E3</item>
<item name="colorM3PressedOverlay">@color/m3_sys_dark_primary</item>
<item name="colorM3Error">#F2B8B5</item>
<item name="colorM3OnError">#601410</item>
<item name="colorM3ErrorContainer">#8C1D18</item>
<item name="colorM3OnErrorContainer">#F9DEDC</item>
</style>
<style name="Theme.Mastodon.Dark.TrueBlack">
@@ -297,6 +305,32 @@
<item name="android:paddingRight">24dp</item>
</style>
<style name="Widget.Mastodon.M3.Button.Text">
<item name="android:background">@drawable/bg_button_m3_text</item>
<item name="android:textColor">@color/button_text_m3_text</item>
<item name="android:paddingLeft">24dp</item>
<item name="android:paddingRight">24dp</item>
</style>
<style name="Widget.Mastodon.M3.Button.Tonal">
<item name="android:background">@drawable/bg_button_m3_tonal</item>
<item name="android:textColor">@color/button_text_m3_text</item>
<item name="android:paddingLeft">24dp</item>
<item name="android:paddingRight">24dp</item>
</style>
<style name="Widget.Mastodon.M3.EditText" parent="android:Widget.Material.EditText">
<item name="android:background">@drawable/bg_m3_outlined_text_field_nopad</item>
<item name="android:textAppearance">@style/m3_body_large</item>
<item name="android:paddingLeft">16dp</item>
<item name="android:paddingRight">16dp</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="android:minHeight">40dp</item>
<item name="android:textColorHint">?colorM3OnSurfaceVariant</item>
<item name="android:textColor">?colorM3OnSurface</item>
</style>
<style name="alert_title">
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textSize">24dp</item>
@@ -316,6 +350,12 @@
<item name="android:lineSpacingExtra">4dp</item>
</style>
<style name="m3_body_small">
<item name="android:textSize">12dp</item>
<item name="android:textColor">?android:textColorSecondary</item>
<item name="android:lineSpacingExtra">2dp</item>
</style>
<style name="m3_title_medium">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">16dp</item>

View File

@@ -1,35 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar-SA"/>
<locale android:name="be-BY"/>
<locale android:name="bn-BD"/>
<locale android:name="bs-BA"/>
<locale android:name="ca-ES"/>
<locale android:name="cs-CZ"/>
<locale android:name="da-DK"/>
<locale android:name="de-DE"/>
<locale android:name="el-GR"/>
<locale android:name="en"/>
<locale android:name="es-ES"/>
<locale android:name="eu-ES"/>
<locale android:name="fa-IR"/>
<locale android:name="fi-FI"/>
<locale android:name="fil-PH"/>
<locale android:name="fr-FR"/>
<locale android:name="ga-IE"/>
<locale android:name="gd-GB"/>
<locale android:name="gl-ES"/>
<locale android:name="hi-IN"/>
<locale android:name="hr-HR"/>
<locale android:name="hu-HU"/>
<locale android:name="hy-AM"/>
<locale android:name="ig-NG"/>
<locale android:name="in-ID"/>
<locale android:name="is-IS"/>
<locale android:name="it-IT"/>
<locale android:name="iw-IL"/>
<locale android:name="ja-JP"/>
<locale android:name="kab"/>
<locale android:name="ko-KR"/>
<locale android:name="my-MM"/>
<locale android:name="nl-NL"/>
<locale android:name="no-NO"/>
<locale android:name="oc-FR"/>
<locale android:name="pl-PL"/>
<locale android:name="pt-BR"/>
<locale android:name="pt-PT"/>
<locale android:name="ro-RO"/>
<locale android:name="ru-RU"/>
<locale android:name="si-LK"/>
<locale android:name="sl-SI"/>
<locale android:name="sv-SE"/>
<locale android:name="th-TH"/>
<locale android:name="tr-TR"/>

View File

@@ -1,11 +1,8 @@
// Run: java tools/GenerateLocaleConfig.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.stream.*;
public class GenerateLocaleConfig{
public static void main(String[] args) throws IOException{
@@ -15,7 +12,7 @@ public class GenerateLocaleConfig{
if(!dir.exists())
throw new RuntimeException("Please run from project directory (can't find mastodon/src/main/res)");
ArrayList<String> locales=new ArrayList<>();
ArrayList<String> locales=new ArrayList<>(), rawLocales=new ArrayList<>();
locales.add("en");
for(File file:dir.listFiles()){
@@ -23,11 +20,13 @@ public class GenerateLocaleConfig{
if(file.isDirectory() && name.startsWith("values-")){
if(new File(file, "strings.xml").exists()){
locales.add(name.substring(name.indexOf('-')+1).replace("-r", "-"));
rawLocales.add(name.substring(name.indexOf('-')+1));
}
}
}
locales.sort(String::compareTo);
rawLocales.sort(String::compareTo);
try(OutputStreamWriter writer=new OutputStreamWriter(new FileOutputStream(new File(dir, "xml/locales_config.xml")), StandardCharsets.UTF_8)){
writer.write("""
<?xml version="1.0" encoding="utf-8"?>
@@ -40,5 +39,25 @@ public class GenerateLocaleConfig{
}
writer.write("</locale-config>");
}
File buildGradle=new File(dir, "../../../build.gradle");
ArrayList<String> buildGradleLines=new ArrayList<>();
try(BufferedReader reader=new BufferedReader(new InputStreamReader(new FileInputStream(buildGradle)))){
String line;
while((line=reader.readLine())!=null){
if(line.trim().startsWith("resConfigs")){
line=line.substring(0, line.indexOf('r'))+"resConfigs ";
line+=rawLocales.stream().map(l->'"'+l+'"').collect(Collectors.joining(", "));
}
buildGradleLines.add(line);
}
}
try(OutputStreamWriter writer=new OutputStreamWriter(new FileOutputStream(buildGradle))){
for(String line:buildGradleLines){
writer.write(line);
writer.write('\n');
}
}
}
}