diff --git a/gradle.properties b/gradle.properties index 52f5917cb..3c6cdff76 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 \ No newline at end of file +android.enableJetifier=false \ No newline at end of file diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index 192cf3543..dde043956 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -41,6 +41,11 @@ public class GlobalUserPreferences{ public static boolean disableAltTextReminder; public static boolean showAltIndicator; public static boolean showNoAltIndicator; + public static boolean enablePreReleases; + public static boolean prefixRepliesWithRe; + public static boolean bottomEncoding; + public static boolean collapseLongPosts; + public static boolean spectatorMode; public static String publishButtonText; public static ThemePreference theme; public static ColorPreference color; @@ -88,9 +93,13 @@ public class GlobalUserPreferences{ keepOnlyLatestNotification=prefs.getBoolean("keepOnlyLatestNotification", false); enableFabAutoHide=prefs.getBoolean("enableFabAutoHide", true); disableAltTextReminder=prefs.getBoolean("disableAltTextReminder", false); - showAltIndicator =prefs.getBoolean("showAltIndicator", true); - showNoAltIndicator =prefs.getBoolean("showNoAltIndicator", true); - enablePreReleases =prefs.getBoolean("enablePreReleases", false); + showAltIndicator=prefs.getBoolean("showAltIndicator", true); + showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true); + enablePreReleases=prefs.getBoolean("enablePreReleases", false); + prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false); + bottomEncoding=prefs.getBoolean("bottomEncoding", false); + collapseLongPosts=prefs.getBoolean("collapseLongPosts", true); + spectatorMode=prefs.getBoolean("spectatorMode", false); publishButtonText=prefs.getString("publishButtonText", ""); theme=ThemePreference.values()[prefs.getInt("theme", 0)]; recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>()); @@ -136,7 +145,11 @@ public class GlobalUserPreferences{ .putBoolean("showAltIndicator", showAltIndicator) .putBoolean("showNoAltIndicator", showNoAltIndicator) .putBoolean("enablePreReleases", enablePreReleases) + .putBoolean("prefixRepliesWithRe", prefixRepliesWithRe) + .putBoolean("collapseLongPosts", collapseLongPosts) + .putBoolean("spectatorMode", spectatorMode) .putString("publishButtonText", publishButtonText) + .putBoolean("bottomEncoding", bottomEncoding) .putInt("theme", theme.ordinal()) .putString("color", color.name()) .putString("recentLanguages", gson.toJson(recentLanguages)) diff --git a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java index 8a4da16bb..e38be7610 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java @@ -155,6 +155,7 @@ public class MainActivity extends FragmentStackActivity{ ); Bundle currentArgs = currentFragment.getArguments(); if (this.fragmentContainers.size() == 1 + && currentArgs != null && currentArgs.getBoolean("_can_go_back", false) && currentArgs.containsKey("account")) { Bundle args = new Bundle(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/SetAccountFollowed.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/SetAccountFollowed.java index 1799120a9..4d7ca8571 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/SetAccountFollowed.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/SetAccountFollowed.java @@ -4,6 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Relationship; public class SetAccountFollowed extends MastodonAPIRequest{ + public SetAccountFollowed(String id, boolean followed, boolean showReblogs){ + this(id, followed, showReblogs, false); + } + public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){ super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class); if(followed) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java index 19a540b09..2c3050ee5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java @@ -15,12 +15,15 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; +import org.joinmastodon.android.utils.StatusFilterPredicate; import org.parceler.Parcels; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import me.grishka.appkit.api.SimpleCallback; @@ -61,6 +64,7 @@ public class AccountTimelineFragment extends StatusListFragment{ @Override public void onSuccess(List result){ if(getActivity()==null) return; + result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList()); onDataLoaded(result, !result.isEmpty()); } }) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 4006f95ed..837929a07 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -59,6 +59,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; + +import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.BaseRecyclerFragment; @@ -79,9 +81,15 @@ public abstract class BaseStatusListFragment exten protected HashMap knownAccounts=new HashMap<>(); protected HashMap relationships=new HashMap<>(); protected Rect tmpRect=new Rect(); + protected ImageButton fab; public BaseStatusListFragment(){ super(20); + if (withComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab); + } + + protected boolean withComposeButton() { + return false; } @Override @@ -95,6 +103,8 @@ public abstract class BaseStatusListFragment exten setRetainInstance(true); } + + @Override protected RecyclerView.Adapter getAdapter(){ return adapter=new DisplayItemsAdapter(); @@ -353,6 +363,13 @@ public abstract class BaseStatusListFragment exten list.setItemAnimator(new BetterItemAnimator()); ((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true); updateToolbar(); + + if (withComposeButton()) { + fab = view.findViewById(R.id.fab); + fab.setVisibility(View.VISIBLE); + fab.setOnClickListener(this::onFabClick); + fab.setOnLongClickListener(this::onFabLongClick); + } } @Override @@ -518,7 +535,7 @@ public abstract class BaseStatusListFragment exten Status status=holder.getItem().status; status.spoilerRevealed=!status.spoilerRevealed; if(!TextUtils.isEmpty(status.spoilerText)){ - TextStatusDisplayItem.Holder text=findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class); + TextStatusDisplayItem.Holder text = findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class); if(text!=null){ adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()); } @@ -527,6 +544,23 @@ public abstract class BaseStatusListFragment exten updateImagesSpoilerState(status, holder.getItemID()); } + public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) { + if (holder.getItem().status.textExpandable != expandable && list != null) { + holder.getItem().status.textExpandable = expandable; + HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class); + if (header != null) header.rebind(); + holder.rebind(); + } + } + + public void onToggleExpanded(Status status, String itemID) { + status.textExpanded = !status.textExpanded; + TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); + HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class); + if (text != null) text.rebind(); + if (header != null) header.rebind(); + } + protected void updateImagesSpoilerState(Status status, String itemID){ ArrayList updatedPositions=new ArrayList<>(); for(ImageStatusDisplayItem.Holder photo:(List)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){ @@ -544,14 +578,13 @@ public abstract class BaseStatusListFragment exten public void onGapClick(GapStatusDisplayItem.Holder item){} - public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warningItem){ - int i = warningItem.getAbsoluteAdapterPosition(); - for(StatusDisplayItem item:warningItem.filteredItems){ - i++; - displayItems.add(i, item); - } - displayItems.remove(warningItem.getAbsoluteAdapterPosition()); - adapter.notifyItemChanged(warningItem.getAbsoluteAdapterPosition()); + public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){ + int startPos = warning.getAbsoluteAdapterPosition(); + displayItems.remove(startPos); + displayItems.addAll(startPos, warning.filteredItems); + adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1); + if (startPos == 0) scrollToTop(); + warning.getItem().status.filterRevealed = true; } public String getAccountID(){ @@ -669,6 +702,16 @@ public abstract class BaseStatusListFragment exten currentPhotoViewer.onPause(); } + protected void onFabClick(View v){ + Bundle args=new Bundle(); + args.putString("account", accountID); + Nav.go(getActivity(), ComposeFragment.class, args); + } + + protected boolean onFabLongClick(View v) { + return UiUtils.pickAccountForCompose(getActivity(), accountID); + } + protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ public DisplayItemsAdapter(){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 40649c9b1..7f28ff9be 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -5,6 +5,7 @@ import static android.os.ext.SdkExtensions.getExtensionVersion; import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages; import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT; import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant; +import static org.joinmastodon.android.ui.utils.UiUtils.isPhotoPickerAvailable; import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages; import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages; @@ -43,6 +44,7 @@ import android.text.TextWatcher; import android.text.format.DateFormat; import android.util.Log; import android.view.Gravity; +import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -67,6 +69,7 @@ import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; +import com.github.bottomSoftwareFoundation.bottom.Bottom; import com.twitter.twittertext.TwitterTextEmojiRegex; import org.joinmastodon.android.E; @@ -116,6 +119,7 @@ import org.joinmastodon.android.ui.views.LinkedTextView; import org.joinmastodon.android.ui.views.ReorderableLinearLayout; import org.joinmastodon.android.ui.views.SizeListenerLinearLayout; import org.joinmastodon.android.utils.MastodonLanguage; +import org.joinmastodon.android.utils.StatusTextEncoder; import org.parceler.Parcel; import org.parceler.Parcels; @@ -156,11 +160,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private static final Pattern GLITCH_LOCAL_ONLY_PATTERN = Pattern.compile("[\\s\\S]*" + GLITCH_LOCAL_ONLY_SUFFIX + "[\uFE00-\uFE0F]*"); private static final String TAG="ComposeFragment"; - private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); + public static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); // from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift - private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?{ + btn.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + if (!GlobalUserPreferences.bottomEncoding) addBottomLanguage(allLanguagesMenu); + return false; + }); + languagePopup.setOnMenuItemClickListener(i->{ if (i.hasSubMenu()) return false; - updateLanguage(allLanguages.get(i.getItemId())); + if (i.getItemId() == allLanguages.size()) { + updateLanguage(language, "\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48", "bottom"); + encoding = "bottom"; + } else { + updateLanguage(allLanguages.get(i.getItemId())); + encoding = null; + } return true; }); } + private void addBottomLanguage(Menu menu) { + if (menu.findItem(allLanguages.size()) == null) { + menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)"); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item){ return true; @@ -1046,6 +1082,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void publish(boolean force){ String text=mainEditText.getText().toString(); CreateStatus.Request req=new CreateStatus.Request(); + if ("bottom".equals(encoding)) { + text = new StatusTextEncoder(Bottom::encode).encode(text); + req.spoilerText = "bottom-encoded emoji spam"; + } if (localOnly && GlobalUserPreferences.accountsInGlitchMode.contains(accountID) && !GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) { @@ -1188,6 +1228,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr List newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)); newRecentLanguages.remove(language); newRecentLanguages.add(0, language); + if (encoding != null) { + newRecentLanguages.remove(encoding); + newRecentLanguages.add(0, encoding); + } + if ("bottom".equals(encoding) && !GlobalUserPreferences.bottomEncoding) { + GlobalUserPreferences.bottomEncoding = true; + GlobalUserPreferences.save(); + } recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList())); GlobalUserPreferences.save(); } @@ -1253,7 +1301,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } private void confirmDiscardDraftAndFinish(){ - new M3AlertDialogBuilder(getActivity()) + boolean attachmentsPending = attachments.stream().anyMatch(att -> att.state != AttachmentUploadState.DONE); + if (attachmentsPending) new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.sk_unfinished_attachments) + .setMessage(R.string.sk_unfinished_attachments_message) + .setPositiveButton(R.string.edit, (d, w) -> {}) + .setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this)) + .show(); + else new M3AlertDialogBuilder(getActivity()) .setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft) .setPositiveButton(R.string.save, (d, w) -> { updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt); @@ -1263,18 +1318,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr .show(); } - /** - * Check to see if Android platform photopicker is available on the device\ - * @return whether the device supports photopicker intents. - */ - private boolean isPhotoPickerAvailable() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return true; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return getExtensionVersion(Build.VERSION_CODES.R) >= 2; - } else - return false; - } /** * Builds the correct intent for the device version to select media. @@ -1286,24 +1329,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr */ private void openFilePicker(boolean photoPicker){ Intent intent; - boolean usePhotoPicker = photoPicker && isPhotoPickerAvailable(); - if (usePhotoPicker) { - intent = new Intent(MediaStore.ACTION_PICK_IMAGES); - intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit()); - } else { - intent = new Intent(Intent.ACTION_GET_CONTENT); + boolean usePhotoPicker=photoPicker && isPhotoPickerAvailable(); + if(usePhotoPicker){ + intent=new Intent(MediaStore.ACTION_PICK_IMAGES); + intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MAX_ATTACHMENTS-getMediaAttachmentsCount()); + }else{ + 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()) { + 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) { + }else{ + if(!usePhotoPicker){ // If photo picker is being used these are the default mimetypes. intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"}); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FabStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FabStatusListFragment.java deleted file mode 100644 index c4443a2c8..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FabStatusListFragment.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.joinmastodon.android.fragments; - -import android.content.res.Configuration; -import android.os.Bundle; -import android.view.View; -import android.widget.ImageButton; - -import org.joinmastodon.android.R; -import org.joinmastodon.android.ui.utils.UiUtils; - -import me.grishka.appkit.Nav; - -public abstract class FabStatusListFragment extends StatusListFragment { - protected ImageButton fab; - - public FabStatusListFragment() { - setListLayoutId(R.layout.recycler_fragment_with_fab); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - fab = view.findViewById(R.id.fab); - fab.setOnClickListener(this::onFabClick); - fab.setOnLongClickListener(this::onFabLongClick); - } - - protected void onFabClick(View v){ - Bundle args=new Bundle(); - args.putString("account", accountID); - Nav.go(getActivity(), ComposeFragment.class, args); - } - - protected boolean onFabLongClick(View v) { - return UiUtils.pickAccountForCompose(getActivity(), accountID); - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java index e0510e5db..ce95a9731 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java @@ -17,12 +17,15 @@ import org.joinmastodon.android.api.requests.tags.GetHashtag; import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed; import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline; import org.joinmastodon.android.events.HashtagUpdatedEvent; +import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.utils.StatusFilterPredicate; import java.util.List; +import java.util.stream.Collectors; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; @@ -33,11 +36,11 @@ import me.grishka.appkit.utils.V; public class HashtagTimelineFragment extends PinnableStatusListFragment { private String hashtag; private boolean following; - private ImageButton fab; private MenuItem followButton; - public HashtagTimelineFragment(){ - setListLayoutId(R.layout.recycler_fragment_with_fab); + @Override + protected boolean withComposeButton() { + return true; } @Override @@ -120,6 +123,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment { @Override public void onSuccess(List result){ if (getActivity() == null) return; + result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); onDataLoaded(result, !result.isEmpty()); } }) @@ -134,14 +138,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - fab=view.findViewById(R.id.fab); - fab.setOnClickListener(this::onFabClick); - fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ')); + protected boolean onFabLongClick(View v) { + return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '); } - private void onFabClick(View v){ + @Override + protected void onFabClick(View v){ Bundle args=new Bundle(); args.putString("account", accountID); args.putString("prefilledText", '#'+hashtag+' '); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java index 669f3d36d..37ce0e002 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java @@ -375,7 +375,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab } private void updateList(List addItems, Map items) { - if (addItems.size() == 0) return; + if (addItems.size() == 0 || getActivity() == null) return; for (int i = 0; i < addItems.size(); i++) items.put(View.generateViewId(), addItems.get(i)); updateOverflowMenu(); } @@ -662,7 +662,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab @Override public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { FrameLayout tabView = tabViews[viewType % getItemCount()]; - ((ViewGroup)tabView.getParent()).removeView(tabView); + ViewGroup tabParent = (ViewGroup) tabView.getParent(); + if (tabParent != null) tabParent.removeView(tabView); tabView.setVisibility(View.VISIBLE); return new SimpleViewHolder(tabView); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java index 4b7e7ef66..195c5eab9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java @@ -32,11 +32,16 @@ import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.utils.V; -public class HomeTimelineFragment extends FabStatusListFragment { +public class HomeTimelineFragment extends StatusListFragment { private HomeTabFragment parent; private String maxID; private String lastSavedMarkerID; + @Override + protected boolean withComposeButton() { + return true; + } + @Override public void onAttach(Activity activity){ super.onAttach(activity); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java index 3198a300d..79422cbb7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java @@ -18,14 +18,17 @@ import org.joinmastodon.android.api.requests.lists.UpdateList; import org.joinmastodon.android.api.requests.timelines.GetListTimeline; import org.joinmastodon.android.events.ListDeletedEvent; import org.joinmastodon.android.events.ListUpdatedCreatedEvent; +import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.ListTimeline; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.TimelineDefinition; import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.ListTimelineEditor; +import org.joinmastodon.android.utils.StatusFilterPredicate; import java.util.List; +import java.util.stream.Collectors; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; @@ -39,10 +42,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment { private String listTitle; @Nullable private ListTimeline.RepliesPolicy repliesPolicy; - private ImageButton fab; - public ListTimelineFragment() { - setListLayoutId(R.layout.recycler_fragment_with_fab); + @Override + protected boolean withComposeButton() { + return true; } @Override @@ -134,10 +137,11 @@ public class ListTimelineFragment extends PinnableStatusListFragment { @Override public void onSuccess(List result) { if (getActivity() == null) return; + result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList()); onDataLoaded(result, !result.isEmpty()); } }) - .exec(accountID); + .exec(accountID); } @Override @@ -148,14 +152,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment { } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - fab=view.findViewById(R.id.fab); - fab.setOnClickListener(this::onFabClick); - fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID)); - } - - private void onFabClick(View v){ + protected void onFabClick(View v){ Bundle args=new Bundle(); args.putString("account", accountID); Nav.go(getActivity(), ComposeFragment.class, args); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java index 2e70881f2..2b7c6a874 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java @@ -99,7 +99,6 @@ public class ListTimelinesFragment extends BaseRecyclerFragment im new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() { @Override public void onSuccess(ListTimeline list) { - saveListMembership(list.id, true); data.add(0, list); adapter.notifyItemRangeInserted(0, 1); E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy)); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index a75fd2476..3516efaed 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -48,6 +48,11 @@ public class NotificationsListFragment extends BaseStatusListFragment items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, Filter.FilterContext.NOTIFICATIONS); + ArrayList items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS); if(titleItem!=null){ for(StatusDisplayItem item:items){ if(item instanceof ImageStatusDisplayItem imgItem){ @@ -193,6 +198,7 @@ public class NotificationsListFragment extends BaseStatusListFragment metadataListData=Collections.emptyList(); + private MetadataAdapter adapter; + private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback()); + private RecyclerView.ViewHolder draggedViewHolder; + private ListImageLoaderWrapper imgLoader; + public ProfileFragment(){ super(R.layout.loader_fragment_overlay_toolbar); } @@ -184,8 +213,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList cover=content.findViewById(R.id.cover); avatarBorder=content.findViewById(R.id.avatar_border); name=content.findViewById(R.id.name); + nameWrap=content.findViewById(R.id.name_wrap); username=content.findViewById(R.id.username); bio=content.findViewById(R.id.bio); + profileCounters=content.findViewById(R.id.profile_counters); followersCount=content.findViewById(R.id.followers_count); followersLabel=content.findViewById(R.id.followers_label); followersBtn=content.findViewById(R.id.followers_btn); @@ -208,6 +239,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList notifyProgress=content.findViewById(R.id.notify_progress); fab=content.findViewById(R.id.fab); followsYouView=content.findViewById(R.id.follows_you); + list=content.findViewById(R.id.metadata); + rolesView=content.findViewById(R.id.roles); noteEdit = content.findViewById(R.id.note_edit); noteWrap = content.findViewById(R.id.note_edit_wrap); @@ -274,7 +307,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList } }; - tabViews=new FrameLayout[5]; + tabViews=new FrameLayout[4]; for(int i=0;iname.getTop()-topBarsH){ - titleTransY=Math.max(0f, titleTransY-(scrollY-(name.getTop()-topBarsH))); + if(scrollY>nameWrap.getTop()-topBarsH){ + titleTransY=Math.max(0f, titleTransY-(scrollY-(nameWrap.getTop()-topBarsH))); } if(toolbarTitleView!=null){ toolbarTitleView.setTranslationY(titleTransY); @@ -827,7 +879,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList case 1 -> postsWithRepliesFragment; case 2 -> pinnedPostsFragment; case 3 -> mediaFragment; - case 4 -> aboutFragment; +// case 4 -> aboutFragment; default -> throw new IllegalStateException(); }; } @@ -892,16 +944,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList invalidateOptionsMenu(); pager.setUserInputEnabled(false); actionButton.setText(R.string.done); - pager.setCurrentItem(4); ArrayList animators=new ArrayList<>(); - for(int i=0;i animators=new ArrayList<>(); actionButton.setText(R.string.edit_profile); - for(int i=0;i(){ @Override public void onSuccess(Account result){ @@ -1148,4 +1199,227 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList return position; } } + + // from ProfileAboutFragment + public void setFields(ArrayList fields){ + metadataListData=fields; + if (isInEditMode) { + isInEditMode=false; + dragHelper.attachToRecyclerView(null); + } + if (adapter != null) adapter.notifyDataSetChanged(); + } + + private class MetadataAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter { + public MetadataAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return switch(viewType){ + case 0 -> new AboutViewHolder(); + case 1 -> new EditableAboutViewHolder(); + case 2 -> new AddRowViewHolder(); + default -> throw new IllegalStateException("Unexpected value: "+viewType); + }; + } + + @Override + public void onBindViewHolder(BaseViewHolder holder, int position){ + if(position { + public BaseViewHolder(int layout){ + super(getActivity(), layout, list); + } + } + + private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder { + private TextView title; + private LinkedTextView value; + + public AboutViewHolder(){ + super(R.layout.item_profile_about); + title=findViewById(R.id.title); + value=findViewById(R.id.value); + } + + @Override + public void onBind(AccountField item){ + title.setText(item.parsedName); + value.setText(item.parsedValue); + if(item.verifiedAt!=null){ + int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63; + value.setTextColor(textColor); + value.setLinkTextColor(textColor); + Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate(); + check.setTint(textColor); + value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null); + }else{ + value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); + value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent)); + value.setCompoundDrawables(null, null, null, null); + } + } + + @Override + public void setImage(int index, Drawable image){ + CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index]; + span.setDrawable(image); + title.invalidate(); + value.invalidate(); + } + + @Override + public void clearImage(int index){ + setImage(index, null); + } + } + + private class EditableAboutViewHolder extends BaseViewHolder { + private EditText title; + private EditText value; + + public EditableAboutViewHolder(){ + super(R.layout.item_profile_about_editable); + title=findViewById(R.id.title); + value=findViewById(R.id.value); + findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{ + dragHelper.startDrag(this); + return true; + }); + title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString())); + value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString())); + findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick); + } + + @Override + public void onBind(AccountField item){ + title.setText(item.name); + value.setText(item.value); + } + + private void onRemoveRowClick(View v){ + int pos=getAbsoluteAdapterPosition(); + metadataListData.remove(pos); + adapter.notifyItemRemoved(pos); + for(int i=0;itoPosition;i--) { + Collections.swap(metadataListData, i, i-1); + } + } + adapter.notifyItemMoved(fromPosition, toPosition); + ((BindableViewHolder)viewHolder).rebind(); + ((BindableViewHolder)target).rebind(); + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){ + + } + + @Override + public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){ + super.onSelectedChanged(viewHolder, actionState); + if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){ + viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw() + viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + draggedViewHolder=viewHolder; + } + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){ + super.clearView(recyclerView, viewHolder); + viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + draggedViewHolder=null; + } + + @Override + public boolean isLongPressDragEnabled(){ + return false; + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java index 263083132..a7462e900 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java @@ -29,11 +29,11 @@ import me.grishka.appkit.api.SimpleCallback; public class ScheduledStatusListFragment extends BaseStatusListFragment { private String nextMaxID; - private ImageButton fab; private static final int SCHEDULED_STATUS_LIST_OPENED = 161; - public ScheduledStatusListFragment() { - setListLayoutId(R.layout.recycler_fragment_with_fab); + @Override + protected boolean withComposeButton() { + return true; } @Override @@ -57,14 +57,24 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment Nav.go(getActivity(), ComposeFragment.class, args)); - fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, args)); + Nav.go(getActivity(), ComposeFragment.class, args); + } + + @Override + protected boolean onFabLongClick(View v) { + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putSerializable("scheduledAt", CreateStatus.getDraftInstant()); + return UiUtils.pickAccountForCompose(getActivity(), accountID, args); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); if (getArguments().getBoolean("hide_fab", false)) fab.setVisibility(View.GONE); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java index ac561161e..0ea11d9d8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java @@ -45,11 +45,13 @@ 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.InstanceRulesFragment; import org.joinmastodon.android.model.Instance; +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; @@ -66,7 +68,6 @@ 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; @@ -222,15 +223,10 @@ public class SettingsFragment extends MastodonToolbarFragment{ GlobalUserPreferences.keepOnlyLatestNotification=i.checked; GlobalUserPreferences.save(); })); -// items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{ -// GlobalUserPreferences.translateButtonOpenedOnly=i.checked; -// GlobalUserPreferences.save(); -// })); -// items.add(new SwitchItem(R.string.sk_settings_hide_translate_in_timeline, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{ -// GlobalUserPreferences.translateButtonOpenedOnly=i.checked; -// GlobalUserPreferences.save(); -// needAppRestart=true; -// })); + items.add(new SwitchItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.prefixRepliesWithRe, i->{ + GlobalUserPreferences.prefixRepliesWithRe=i.checked; + GlobalUserPreferences.save(); + })); items.add(new HeaderItem(R.string.sk_timelines)); items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{ @@ -264,6 +260,19 @@ public class SettingsFragment extends MastodonToolbarFragment{ items.add(new SwitchItem(R.string.sk_settings_show_no_alt_indicator, R.drawable.ic_fluent_important_24_regular, GlobalUserPreferences.showNoAltIndicator, i->{ GlobalUserPreferences.showNoAltIndicator=i.checked; GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_settings_collapse_long_posts, R.drawable.ic_fluent_chevron_down_24_filled, GlobalUserPreferences.collapseLongPosts, i->{ + GlobalUserPreferences.collapseLongPosts=i.checked; + GlobalUserPreferences.save(); + })); + items.add(new SwitchItem(R.string.sk_spectator_mode, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{ + GlobalUserPreferences.spectatorMode=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{ + GlobalUserPreferences.translateButtonOpenedOnly=i.checked; + GlobalUserPreferences.save(); needAppRestart=true; })); @@ -360,7 +369,20 @@ public class SettingsFragment extends MastodonToolbarFragment{ })); // items.add(new TextItem(R.string.log_out, this::confirmLogOut)); - items.add(new FooterItem(getString(R.string.mo_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE))); + 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.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE))); } private void updatePublishText(Button btn) { @@ -645,7 +667,7 @@ public class SettingsFragment extends MastodonToolbarFragment{ this.text=getString(text); } - public HeaderItem(String text) { + public HeaderItem(String text){ this.text=text; } @@ -763,6 +785,11 @@ public class SettingsFragment extends MastodonToolbarFragment{ this.secondaryText = secondaryText; } + public TextItem(String text, Runnable onClick){ + this.text=text; + this.onClick=onClick; + } + @Override public int getViewType(){ return 4; @@ -775,6 +802,10 @@ public class SettingsFragment extends MastodonToolbarFragment{ super(text); } + public RedHeaderItem(String text){ + super(text); + } + @Override public int getViewType(){ return 5; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java index b8e68b272..7293a2fac 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java @@ -6,6 +6,7 @@ import android.os.Bundle; import com.squareup.otto.Subscribe; import org.joinmastodon.android.E; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; @@ -31,7 +32,9 @@ public abstract class StatusListFragment extends BaseStatusListFragment{ protected EventListener eventListener=new EventListener(); protected List buildDisplayItems(Status s){ - return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null, Filter.FilterContext.HOME); + boolean addFooter = !GlobalUserPreferences.spectatorMode || + (this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id)); + return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java index da9918ece..03cadb3b0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java @@ -26,7 +26,7 @@ import java.util.stream.Collectors; import me.grishka.appkit.api.SimpleCallback; public class ThreadFragment extends StatusListFragment{ - private Status mainStatus; + protected Status mainStatus; @Override public void onCreate(Bundle savedInstanceState){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java index aea419590..c70e83129 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java @@ -129,7 +129,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment result){ if (getActivity() == null) return; + result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); onDataLoaded(result, !result.isEmpty()); } }).exec(accountID); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java index 32f5d0603..3729d9245 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java @@ -5,7 +5,6 @@ import android.view.View; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; -import org.joinmastodon.android.fragments.FabStatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Status; @@ -17,10 +16,16 @@ import java.util.stream.Collectors; import me.grishka.appkit.api.SimpleCallback; -public class FederatedTimelineFragment extends FabStatusListFragment { +public class FederatedTimelineFragment extends StatusListFragment { private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE); private String maxID; + @Override + protected boolean withComposeButton() { + return true; + } + + @Override protected void doLoadData(int offset, int count){ currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count) @@ -30,7 +35,8 @@ public class FederatedTimelineFragment extends FabStatusListFragment { if(!result.isEmpty()) maxID=result.get(result.size()-1).id; if (getActivity() == null) return; - onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty()); + result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); + onDataLoaded(result, !result.isEmpty()); } }) .exec(accountID); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java index fb2a2ac22..dc2dff4c9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java @@ -3,9 +3,7 @@ package org.joinmastodon.android.fragments.discover; import android.os.Bundle; import android.view.View; -import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; -import org.joinmastodon.android.fragments.FabStatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Status; @@ -17,10 +15,16 @@ import java.util.stream.Collectors; import me.grishka.appkit.api.SimpleCallback; -public class LocalTimelineFragment extends FabStatusListFragment { -// private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE); +public class LocalTimelineFragment extends StatusListFragment { + private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE); private String maxID; + @Override + protected boolean withComposeButton() { + return true; + } + + @Override protected void doLoadData(int offset, int count){ currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count) @@ -30,7 +34,8 @@ public class LocalTimelineFragment extends FabStatusListFragment { if(!result.isEmpty()) maxID=result.get(result.size()-1).id; if (getActivity() == null) return; - onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty()); + result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()); + onDataLoaded(result, !result.isEmpty()); } }) .exec(accountID); @@ -39,6 +44,6 @@ public class LocalTimelineFragment extends FabStatusListFragment { @Override public void onViewCreated(View view, Bundle savedInstanceState){ super.onViewCreated(view, savedInstanceState); -// bannerHelper.maybeAddBanner(contentWrap); + bannerHelper.maybeAddBanner(contentWrap); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java index fcbe081bd..55b21c530 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java @@ -193,30 +193,24 @@ public class AccountActivationFragment extends ToolbarFragment{ mgr.removeAccount(accountID); 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(); } } @@ -249,4 +243,11 @@ public class AccountActivationFragment extends ToolbarFragment{ 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(), OnboardingProfileSetupFragment.class, args); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java index f4793b8c1..5f0404ac2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/GoogleMadeMeAddThisFragment.java @@ -63,6 +63,8 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{ private ItemsAdapter itemsAdapter; private ElevationOnScrollListener onScrollListener; + private static final int SIGNUP_REQUEST=722; + @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); @@ -139,7 +141,16 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{ 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 diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java index 2d17d6272..7c463f191 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java @@ -92,7 +92,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment{ public InstancesAdapter(){ super(imgLoader); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java new file mode 100644 index 000000000..f1ca5c4f7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java @@ -0,0 +1,350 @@ +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{ + private String accountID; + private Map 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 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 result){ + relationshipsRequest=null; + relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity())); + if(list==null) + return; + for(int i=0;i=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 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 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){ + 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(), HomeFragment.class, args); + getActivity().getWindow().getDecorView().postDelayed(()->Nav.finish(this), 500); + } + + @Override + protected boolean canGoBack(){ + return true; + } + + @Override + public void onToolbarNavigationClick(){ + Bundle args=new Bundle(); + args.putString("account", accountID); + Nav.goClearingStack(getActivity(), HomeFragment.class, args); + } + + private class SuggestionsAdapter extends UsableRecyclerView.Adapter 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 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); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingProfileSetupFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingProfileSetupFragment.java new file mode 100644 index 000000000..a5f1e83c8 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingProfileSetupFragment.java @@ -0,0 +1,237 @@ +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 fieldTitles=savedInstanceState.getStringArrayList("fieldTitles"); + ArrayList fieldValues=savedInstanceState.getStringArrayList("fieldValues"); + for(int i=0;i{ + 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 fields=new ArrayList<>(); + for(int i=0;i(){ + @Override + public void onSuccess(Account result){ + AccountSessionManager.getInstance().updateAccountInfo(accountID, result); + Bundle args=new Bundle(); + args.putString("account", accountID); + Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args); + getActivity().getWindow().getDecorView().postDelayed(()->Nav.finish(OnboardingProfileSetupFragment.this), 500); + } + + @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; + }); + return view; + } + + @Override + public void onSwapItems(int oldIndex, int newIndex){} + + @Override + public void onSaveInstanceState(Bundle outState){ + super.onSaveInstanceState(outState); + ArrayList fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>(); + for(int i=0;i errorFields=new HashSet<>(); + private ElevationOnScrollListener onScrollListener; @Override public void onCreate(Bundle savedInstanceState){ @@ -145,19 +150,22 @@ public class SignupFragment extends ToolbarFragment{ super.onViewCreated(view, savedInstanceState); 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 + @Override protected void onUpdateToolbar(){ -// super.onUpdateToolbar(); - getToolbar().setBackground(null); + 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())){ - passwordConfirm.setError(getString(R.string.signup_passwords_dont_match)); - passwordConfirmWrap.setErrorState(); + passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match)); return; } showProgressDialog(); @@ -212,8 +220,22 @@ public class SignupFragment extends ToolbarFragment{ anyFieldsSkipped=true; continue; } - field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n"))); - getFieldWrapByName(fieldName).setErrorState(); + List 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; @@ -231,6 +253,40 @@ public class SignupFragment extends ToolbarFragment{ .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; @@ -323,6 +379,11 @@ public class SignupFragment extends ToolbarFragment{ } } + private void onGoBackLinkClick(LinkSpan span){ + setResult(false, null); + Nav.finish(this); + } + private class ErrorClearingListener implements TextWatcher{ public final EditText editText; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Account.java b/mastodon/src/main/java/org/joinmastodon/android/model/Account.java index 18e077c63..9f996d769 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Account.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Account.java @@ -133,6 +133,14 @@ public class Account extends BaseModel{ */ public Instant muteExpiresAt; + public List roles; + + @Parcel + public static class Role { + public String name; + /** #rrggbb */ + public String color; + } @Override public void postprocess() throws ObjectValidationException{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java b/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java index ea34b8402..241b82187 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java @@ -21,6 +21,7 @@ public class Filter extends BaseModel{ public String title; @RequiredField public String phrase; + public String title; public transient EnumSet context=EnumSet.noneOf(FilterContext.class); public Instant expiresAt; public boolean irreversible; @@ -52,6 +53,7 @@ public class Filter extends BaseModel{ else pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE); } + if (title == null) title = phrase; return pattern.matcher(text).find(); } @@ -80,7 +82,9 @@ public class Filter extends BaseModel{ @SerializedName("public") PUBLIC, @SerializedName("thread") - THREAD + THREAD, + @SerializedName("account") + ACCOUNT } public enum FilterAction{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java index 3db9d1433..b1cc26252 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java @@ -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 languages; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java b/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java new file mode 100644 index 000000000..751b2c0ed --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/ParsedAccount.java @@ -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)); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java b/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java index 837178fef..e5f6ec13e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Relationship.java @@ -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{"+ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/ScheduledStatus.java b/mastodon/src/main/java/org/joinmastodon/android/model/ScheduledStatus.java index 02746c771..f4ee206f0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/ScheduledStatus.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/ScheduledStatus.java @@ -62,19 +62,13 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{ } public Status toStatus() { - Status s = new Status(); - s.id = id; + Status s = Status.ofFake(id, params.text, scheduledAt); s.mediaAttachments = mediaAttachments; - s.createdAt = scheduledAt; s.inReplyToId = params.inReplyToId > 0 ? "" + params.inReplyToId : null; - s.content = s.text = params.text; s.spoilerText = params.spoilerText; s.visibility = params.visibility; s.language = params.language; s.sensitive = params.sensitive; - s.mentions = List.of(); - s.tags = List.of(); - s.emojis = List.of(); if (params.poll != null) s.poll = params.poll.toPoll(); return s; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java index da9f3393e..41d11348c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java @@ -58,9 +58,9 @@ public class Status extends BaseModel implements DisplayItemsParent{ public boolean bookmarked; public boolean pinned; - public boolean filterRevealed; - + public transient boolean filterRevealed; public transient boolean spoilerRevealed; + public transient boolean textExpanded, textExpandable; public transient boolean hasGapAfter; private transient String strippedText; @@ -160,6 +160,7 @@ public class Status extends BaseModel implements DisplayItemsParent{ s.mentions = List.of(); s.tags = List.of(); s.emojis = List.of(); + s.filtered = List.of(); return s; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index 239641419..9d075ea3e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -23,6 +23,8 @@ import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.StringRes; + import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; @@ -140,7 +142,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ private final TextView name, username, timestamp, extraText, separator; - private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, botIcon; + private final View collapseBtn; + private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator; private final PopupMenu optionsMenu; private Relationship relationship; private APIRequest currentRelationshipRequest; @@ -163,6 +166,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ visibility=findViewById(R.id.visibility); deleteNotification=findViewById(R.id.delete_notification); unreadIndicator=findViewById(R.id.unread_indicator); + collapseBtn=findViewById(R.id.collapse_btn); + collapseBtnIcon=findViewById(R.id.collapse_btn_icon); botIcon=findViewById(R.id.bot_icon); extraText=findViewById(R.id.extra_text); avatar.setOnClickListener(this::onAvaClick); @@ -175,6 +180,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ fragment.removeNotification(item.notification); } })); + collapseBtn.setOnClickListener(l -> item.parentFragment.onToggleExpanded(item.status, getItemID())); optionsMenu=new PopupMenu(activity, more); @@ -326,7 +332,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault()); timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter)); } - else if ((item.status==null || item.status.editedAt==null) && item.createdAt != null) + else if ((!item.inset || item.status==null || item.status.editedAt==null) && item.createdAt != null) timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt)); else if (item.status != null && item.status.editedAt != null) timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt))); @@ -400,6 +406,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ more.setContentDescription(desc); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) more.setTooltipText(desc); + if (item.status == null || !item.status.textExpandable) { + collapseBtn.setVisibility(View.GONE); + } else { + String collapseText = item.parentFragment.getString(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand); + collapseBtn.setVisibility(item.status.textExpandable ? View.VISIBLE : View.GONE); + collapseBtn.setContentDescription(collapseText); + if (GlobalUserPreferences.reduceMotion) collapseBtnIcon.setScaleY(item.status.textExpanded ? -1 : 1); + else collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText); + } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java index 782f94e17..b76cb997b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java @@ -1,12 +1,21 @@ 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.graphics.drawable.Drawable; 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.ImageView; +import android.widget.TextView; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Attachment; @@ -19,6 +28,7 @@ import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout; import androidx.annotation.LayoutRes; import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.utils.CubicBezierInterpolator; public abstract class ImageStatusDisplayItem extends StatusDisplayItem{ public final int index; @@ -56,11 +66,35 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{ private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); private boolean didClear; + private AnimatorSet currentAnim; + private final FrameLayout altTextWrapper; + private final TextView altTextButton; + private final ImageView noAltTextButton; + private final View altTextScroller; + private final ImageButton altTextClose; + private final TextView altText, noAltText; + + private View altOrNoAltButton; + private boolean altTextShown; + public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){ super(activity, layout, parent); photo=findViewById(R.id.photo); photo.setOnClickListener(this::onViewClick); this.layout=(ImageAttachmentFrameLayout)itemView; + + altTextWrapper=findViewById(R.id.alt_text_wrapper); + altTextButton=findViewById(R.id.alt_button); + noAltTextButton=findViewById(R.id.no_alt_button); + altTextScroller=findViewById(R.id.alt_text_scroller); + altTextClose=findViewById(R.id.alt_text_close); + altText=findViewById(R.id.alt_text); + noAltText=findViewById(R.id.no_alt_text); + + altTextButton.setOnClickListener(this::onShowHideClick); + noAltTextButton.setOnClickListener(this::onShowHideClick); + altTextClose.setOnClickListener(this::onShowHideClick); +// altTextScroller.setNestedScrollingEnabled(true); } @Override @@ -73,6 +107,111 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{ photo.setImageDrawable(crossfadeDrawable); photo.setContentDescription(TextUtils.isEmpty(item.attachment.description) ? item.parentFragment.getString(R.string.media_no_description) : item.attachment.description); didClear=false; + + if (currentAnim != null) currentAnim.cancel(); + + boolean altTextMissing = TextUtils.isEmpty(item.attachment.description); + altOrNoAltButton = altTextMissing ? noAltTextButton : altTextButton; + altTextShown=false; + + altTextScroller.setVisibility(View.GONE); + altTextClose.setVisibility(View.GONE); + altTextButton.setVisibility(View.VISIBLE); + noAltTextButton.setVisibility(View.VISIBLE); + altTextButton.setAlpha(1f); + noAltTextButton.setAlpha(1f); + altTextWrapper.setVisibility(View.VISIBLE); + + if (altTextMissing){ + if (GlobalUserPreferences.showNoAltIndicator) { + noAltTextButton.setVisibility(View.VISIBLE); + noAltText.setVisibility(View.VISIBLE); + altTextWrapper.setBackgroundResource(R.drawable.bg_image_no_alt_overlay); + altTextButton.setVisibility(View.GONE); + altText.setVisibility(View.GONE); + } else { + altTextWrapper.setVisibility(View.GONE); + } + }else{ + if (GlobalUserPreferences.showAltIndicator) { + noAltTextButton.setVisibility(View.GONE); + noAltText.setVisibility(View.GONE); + altTextWrapper.setBackgroundResource(R.drawable.bg_image_alt_overlay); + altTextButton.setVisibility(View.VISIBLE); + altTextButton.setText(R.string.sk_alt_button); + altText.setVisibility(View.VISIBLE); + altText.setText(item.attachment.description); + altText.setPadding(0, 0, 0, 0); + } else { + altTextWrapper.setVisibility(View.GONE); + } + } + } + + private void onShowHideClick(View v){ + boolean show=v.getId()==R.id.alt_button || v.getId()==R.id.no_alt_button; + + if(altTextShown==show) + return; + if(currentAnim!=null) + currentAnim.cancel(); + + altTextShown=show; + if(show){ + altTextScroller.setVisibility(View.VISIBLE); + altTextClose.setVisibility(View.VISIBLE); + }else{ + altOrNoAltButton.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(altOrNoAltButton, 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){ + altOrNoAltButton.setVisibility(View.GONE); + }else{ + altTextScroller.setVisibility(View.GONE); + altTextClose.setVisibility(View.GONE); + } + currentAnim=null; + } + }); + set.start(); + currentAnim=set; + + return true; + } + }); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java index cc5882dc2..a0609d10c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java @@ -37,150 +37,9 @@ public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{ return Type.PHOTO; } - public static class Holder extends ImageStatusDisplayItem.Holder{ - private final FrameLayout altTextWrapper; - private final TextView altTextButton; - private final ImageView noAltTextButton; - private final View altTextScroller; - private final ImageButton altTextClose; - private final TextView altText, noAltText; - - private View altOrNoAltButton; - private boolean altTextShown; - private AnimatorSet currentAnim; - - public Holder(Activity activity, ViewGroup parent){ + public static class Holder extends ImageStatusDisplayItem.Holder { + 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); - noAltTextButton=findViewById(R.id.no_alt_button); - altTextScroller=findViewById(R.id.alt_text_scroller); - altTextClose=findViewById(R.id.alt_text_close); - altText=findViewById(R.id.alt_text); - noAltText=findViewById(R.id.no_alt_text); - - altTextButton.setOnClickListener(this::onShowHideClick); - noAltTextButton.setOnClickListener(this::onShowHideClick); - altTextClose.setOnClickListener(this::onShowHideClick); -// altTextScroller.setNestedScrollingEnabled(true); - } - - @Override - public void onBind(ImageStatusDisplayItem item){ - super.onBind(item); - boolean altTextMissing = TextUtils.isEmpty(item.attachment.description); - altOrNoAltButton = altTextMissing ? noAltTextButton : altTextButton; - altTextShown=false; - if(currentAnim!=null) - currentAnim.cancel(); - - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - altTextButton.setVisibility(View.VISIBLE); - noAltTextButton.setVisibility(View.VISIBLE); - altTextButton.setAlpha(1f); - noAltTextButton.setAlpha(1f); - altTextWrapper.setVisibility(View.VISIBLE); - - if (altTextMissing){ - if (GlobalUserPreferences.showNoAltIndicator) { - noAltTextButton.setVisibility(View.VISIBLE); - noAltText.setVisibility(View.VISIBLE); - altTextWrapper.setBackgroundResource(R.drawable.bg_image_no_alt_overlay); - altTextButton.setVisibility(View.GONE); - altText.setVisibility(View.GONE); - } else { - altTextWrapper.setVisibility(View.GONE); - } - }else{ - if (GlobalUserPreferences.showAltIndicator) { - noAltTextButton.setVisibility(View.GONE); - noAltText.setVisibility(View.GONE); - altTextWrapper.setBackgroundResource(R.drawable.bg_image_alt_overlay); - altTextButton.setVisibility(View.VISIBLE); - altTextButton.setText(R.string.sk_alt_button); - altText.setVisibility(View.VISIBLE); - altText.setText(item.attachment.description); - altText.setPadding(0, 0, 0, 0); - } else { - altTextWrapper.setVisibility(View.GONE); - } - } - - if(!item.status.filterRevealed){ - this.itemView.setVisibility(View.GONE); - ViewGroup.LayoutParams params = this.itemView.getLayoutParams(); - params.height = 0; - params.width = 0; - this.itemView.setLayoutParams(params); -// item.parentFragment.notifyItemsChanged(this.getAbsoluteAdapterPosition()); - } - } - - private void onShowHideClick(View v){ - boolean show=v.getId()==R.id.alt_button || v.getId()==R.id.no_alt_button; - - if(altTextShown==show) - return; - if(currentAnim!=null) - currentAnim.cancel(); - - altTextShown=show; - if(show){ - altTextScroller.setVisibility(View.VISIBLE); - altTextClose.setVisibility(View.VISIBLE); - }else{ - altOrNoAltButton.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(altOrNoAltButton, 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){ - altOrNoAltButton.setVisibility(View.GONE); - }else{ - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - } - currentAnim=null; - } - }); - set.start(); - currentAnim=set; - - return true; - } - }); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 1f063977b..bd2408e3e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -81,34 +81,40 @@ public abstract class StatusDisplayItem{ case ACCOUNT -> new AccountStatusDisplayItem.Holder(activity, parent); case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent); case GAP -> new GapStatusDisplayItem.Holder(activity, parent); - case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent); case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent); + case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent); }; } - public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){ + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification){ + return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, Filter.FilterContext.HOME); + } + + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){ return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext); } - public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){ + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){ + return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, Filter.FilterContext.HOME); + } + + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){ String parentID=parentObject.getID(); ArrayList items=new ArrayList<>(); - ArrayList filtered=new ArrayList<>(); - Status statusForContent=status.getContentStatus(); Bundle args=new Bundle(); args.putString("account", accountID); ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null; - List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(filterContext)).collect(Collectors.toList()); + List filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream() + .filter(f -> f.context.contains(filterContext)).collect(Collectors.toList()); StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters); if(!statusForContent.filterRevealed){ statusForContent.filterRevealed = filterPredicate.testWithWarning(status); } - if(status.reblog!=null){ boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account); items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{ @@ -189,9 +195,10 @@ public abstract class StatusDisplayItem{ item.index=i++; } - if(!statusForContent.filterRevealed){ - filtered.add(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)); - return filtered; + if (!statusForContent.filterRevealed) { + return new ArrayList<>(List.of( + new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items) + )); } return items; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index 256d2b6f6..c38395635 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -3,14 +3,17 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; import android.text.TextUtils; -import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Button; +import android.widget.ScrollView; import android.widget.TextView; +import android.widget.Toast; + +import com.github.bottomSoftwareFoundation.bottom.Bottom; +import com.github.bottomSoftwareFoundation.bottom.TranslationError; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.statuses.TranslateStatus; @@ -19,12 +22,14 @@ import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable; import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.TranslatedStatus; import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.views.LinkedTextView; +import org.joinmastodon.android.utils.StatusTextEncoder; + +import java.util.regex.Pattern; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; @@ -44,6 +49,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ public boolean translated = false; public TranslatedStatus translation = null; private AccountSession session; + public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48"); public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){ super(parentID, parentFragment); @@ -81,10 +87,14 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ private final LinkedTextView text; private final LinearLayout spoilerHeader; - private final TextView spoilerTitle, spoilerTitleInline, translateInfo; - private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress; - private final Drawable backgroundColor, borderColor; + private final TextView spoilerTitle, spoilerTitleInline, translateInfo, readMore; + private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress, spaceBelowText; + private final int backgroundColor, borderColor; private final Button translateButton; + private final ScrollView textScrollView; + + private final float textMaxHeight, textCollapsedHeight; + private final LinearLayout.LayoutParams collapseParams, wrapParams; public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_text, parent); @@ -101,14 +111,16 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ translateInfo=findViewById(R.id.translate_info); translateProgress=findViewById(R.id.translate_progress); itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this)); - - TypedValue outValue=new TypedValue(); - activity.getTheme().resolveAttribute(R.attr.colorBackgroundLight, outValue, true); - backgroundColor=activity.getDrawable(outValue.resourceId); -// activity.getTheme().resolveAttribute(R.attr.colorBackgroundLightest, outValue, true); -// backgroundColorInset=activity.getDrawable(outValue.resourceId); - activity.getTheme().resolveAttribute(R.attr.colorPollVoted, outValue, true); - borderColor=activity.getDrawable(outValue.resourceId); + backgroundColor=UiUtils.getThemeColor(activity, R.attr.colorBackgroundLight); + borderColor=UiUtils.getThemeColor(activity, R.attr.colorPollVoted); + textScrollView=findViewById(R.id.text_scroll_view); + readMore=findViewById(R.id.read_more); + spaceBelowText=findViewById(R.id.space_below_text); + textMaxHeight=activity.getResources().getDimension(R.dimen.text_max_height); + textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height); + collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight); + wrapParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + readMore.setOnClickListener(v -> item.parentFragment.onToggleExpanded(item.status, getItemID())); } @Override @@ -117,6 +129,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ ? HtmlParser.parse(item.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID()) : item.text); text.setTextIsSelectable(item.textSelectable); + if (item.textSelectable) { + textScrollView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + } spoilerTitleInline.setTextIsSelectable(item.textSelectable); text.setInvalidateOnEveryFrame(false); spoilerTitleInline.setBackground(item.inset ? null : backgroundColor); @@ -149,18 +164,30 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled; - translateWrap.setVisibility(translateEnabled && - !item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) && - item.status.language != null && - (item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage)) - ? View.VISIBLE : View.GONE); + boolean isBottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find(); + boolean translateVisible = (isBottomText || ( + translateEnabled && + !item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) && + item.status.language != null && + (item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage)))) + && (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable); + translateWrap.setVisibility(translateVisible ? View.VISIBLE : View.GONE); translateButton.setText(item.translated ? R.string.sk_translate_show_original : R.string.sk_translate_post); - translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, item.translation.provider) : ""); - - - + translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, isBottomText ? "bottom-java" : item.translation.provider) : ""); translateButton.setOnClickListener(v->{ if (item.translation == null) { + if (isBottomText) { + try { + item.translation = new TranslatedStatus(); + item.translation.content = new StatusTextEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN); + item.translated = true; + } catch (TranslationError err) { + item.translation = null; + Toast.makeText(itemView.getContext(), err.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); + } + rebind(); + return; + } translateProgress.setVisibility(View.VISIBLE); translateButton.setClickable(false); translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start(); @@ -190,6 +217,25 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ } }); + readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand); + spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE); + + if (!GlobalUserPreferences.collapseLongPosts) { + textScrollView.setLayoutParams(wrapParams); + readMore.setVisibility(View.GONE); + } + + if (GlobalUserPreferences.collapseLongPosts) text.post(() -> { + boolean tooBig = text.getMeasuredHeight() > textMaxHeight; + boolean inTimeline = !item.textSelectable; + boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText); + boolean expandable = inTimeline && tooBig && !hasSpoiler; + item.parentFragment.onEnableExpandable(this, expandable); + }); + + readMore.setVisibility(item.status.textExpandable && !item.status.textExpanded ? View.VISIBLE : View.GONE); + textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams); + if (item.status.textExpandable && !translateVisible) spaceBelowText.setVisibility(View.VISIBLE); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java index 33906813a..d30457050 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/tabs/TabLayout.java @@ -602,7 +602,7 @@ public class TabLayout extends HorizontalScrollView { *

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 diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java index fee86b22a..6ada07785 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java @@ -18,6 +18,10 @@ public class LinkSpan extends CharacterStyle { private String accountID; private String text; + public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){ + this(link, listener, type, accountID, null); + } + public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){ this.listener=listener; this.link=link; @@ -40,6 +44,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, null); + case CUSTOM -> listener.onLinkClick(this); } } @@ -73,6 +78,7 @@ public class LinkSpan extends CharacterStyle { public enum Type{ URL, MENTION, - HASHTAG + HASHTAG, + CUSTOM } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index fdb3661dc..6c6b31595 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -28,6 +28,9 @@ 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; @@ -658,8 +661,40 @@ public class UiUtils{ }).exec(accountID); } - public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer progressCallback, Consumer resultCallback){ + 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(); + ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor}); + if(relationship.blocking) + button.setTextColor(button.getResources().getColorStateList(R.color.error_600)); + else + button.setTextColor(ta.getColorStateList(0)); + ta.recycle(); + } + + public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer progressCallback, Consumer resultCallback) { + if (relationship.blocking) { confirmToggleBlockUser(activity, accountID, account, true, resultCallback); }else if(relationship.muting){ confirmToggleMuteUser(activity, accountID, account, true, resultCallback); @@ -1163,11 +1198,62 @@ public class UiUtils{ return container; } - 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); + } + } + }; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java index bb92feeef..e5fb7292c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java @@ -20,6 +20,7 @@ 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; @@ -47,6 +48,7 @@ public class FloatingHintEditTextLayout extends FrameLayout{ private RectF tmpRect=new RectF(); private ColorStateList labelColors, origHintColors; private boolean errorState; + private TextView errorView; public FloatingHintEditTextLayout(Context context){ this(context, null); @@ -95,12 +97,22 @@ 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)); + setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field, getContext().getTheme())); refreshDrawableState(); } boolean newHintVisible=text.length()==0; @@ -211,12 +223,34 @@ public class FloatingHintEditTextLayout extends FrameLayout{ label.setTextColor(color.getColorForState(getDrawableState(), 0xff00ff00)); } - public void setErrorState(){ + 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{ @@ -313,8 +347,8 @@ public class FloatingHintEditTextLayout extends FrameLayout{ @Override protected void onBoundsChange(@NonNull Rect bounds){ super.onBoundsChange(bounds); - LayoutParams lp=(LayoutParams) edit.getLayoutParams(); - wrapped.setBounds(bounds.left+lp.leftMargin-V.dp(12), bounds.top, bounds.right-lp.rightMargin+V.dp(12), bounds.bottom); + int offset=V.dp(12); + wrapped.setBounds(edit.getLeft()-offset, edit.getTop()-offset, edit.getRight()+offset, edit.getBottom()+offset); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/UntouchableScrollView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/UntouchableScrollView.java new file mode 100644 index 000000000..6b228cdde --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/UntouchableScrollView.java @@ -0,0 +1,30 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.ScrollView; + +public class UntouchableScrollView extends ScrollView { + public UntouchableScrollView(Context context) { + super(context); + } + + public UntouchableScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public UntouchableScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public UntouchableScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + super.onTouchEvent(event); + return false; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java b/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java index 94c38166f..b59425085 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/ElevationOnScrollListener.java @@ -71,7 +71,7 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp @Override public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){ - handleScroll(v.getContext(), scrollY==0); + handleScroll(v.getContext(), scrollY<=0); } private void handleScroll(Context context, boolean newAtTop){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusTextEncoder.java b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusTextEncoder.java new file mode 100644 index 000000000..12f3d4891 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusTextEncoder.java @@ -0,0 +1,58 @@ +package org.joinmastodon.android.utils; + +import android.text.TextUtils; + +import org.joinmastodon.android.fragments.ComposeFragment; + +import java.util.function.Function; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// not a good class +public class StatusTextEncoder { + private final Function fn; + + // see ComposeFragment.HIGHLIGHT_PATTERN + private final static Pattern EXCLUDE_PATTERN = Pattern.compile("\\s*(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))\\s*"); + + public StatusTextEncoder(Function fn) { + this.fn = fn; + } + + // prettiest method award winner 2023 [citation needed] + public String encode(String content) { + StringBuilder encodedString = new StringBuilder(); + // matches mentions and hashtags + Matcher m = EXCLUDE_PATTERN.matcher(content); + int previousEnd = 0; + while (m.find()) { + MatchResult res = m.toMatchResult(); + // everything before the match - do encode + encodedString.append(fn.apply(content.substring(previousEnd, res.start()))); + previousEnd = res.end(); + // the match - do not encode + encodedString.append(res.group()); + } + // everything after the last match - do encode + encodedString.append(fn.apply(content.substring(previousEnd))); + return encodedString.toString(); + } + + // prettiest almost-exact replica of a pretty function + public String decode(String content, Pattern regex) { + Matcher m = regex.matcher(content); + StringBuilder decodedString = new StringBuilder(); + int previousEnd = 0; + while (m.find()) { + MatchResult res = m.toMatchResult(); + // everything before the match - do not decode + decodedString.append(content.substring(previousEnd, res.start())); + previousEnd = res.end(); + // the match - do decode + decodedString.append(fn.apply(res.group())); + } + decodedString.append(content.substring(previousEnd)); + return decodedString.toString(); + } +} diff --git a/mastodon/src/main/res/color/button_text_m3_tonal.xml b/mastodon/src/main/res/color/button_text_m3_tonal.xml new file mode 100644 index 000000000..cce583679 --- /dev/null +++ b/mastodon/src/main/res/color/button_text_m3_tonal.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_button_m3_tonal.xml b/mastodon/src/main/res/drawable/bg_button_m3_tonal.xml new file mode 100644 index 000000000..d6043fc37 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_button_m3_tonal.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_onboarding_avatar.xml b/mastodon/src/main/res/drawable/bg_onboarding_avatar.xml new file mode 100644 index 000000000..924918801 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_onboarding_avatar.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_pill.xml b/mastodon/src/main/res/drawable/bg_pill.xml new file mode 100644 index 000000000..af6a69a4f --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_pill.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_add_24px.xml b/mastodon/src/main/res/drawable/ic_add_24px.xml new file mode 100644 index 000000000..b234bf84a --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_add_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_add_photo_alternate_48px.xml b/mastodon/src/main/res/drawable/ic_add_photo_alternate_48px.xml new file mode 100644 index 000000000..e7ffd70e0 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_add_photo_alternate_48px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_drag_handle_24px.xml b/mastodon/src/main/res/drawable/ic_drag_handle_24px.xml new file mode 100644 index 000000000..7428dd8b4 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_drag_handle_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_chevron_down_20_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_chevron_down_20_filled.xml new file mode 100644 index 000000000..c5dceeb7d --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_chevron_down_20_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_chevron_down_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_chevron_down_24_filled.xml new file mode 100644 index 000000000..7916b3932 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_chevron_down_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_more_horizontal_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_more_horizontal_24_regular.xml new file mode 100644 index 000000000..de1128305 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_more_horizontal_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/alt_badge.xml b/mastodon/src/main/res/layout/alt_badge.xml new file mode 100644 index 000000000..f165466cf --- /dev/null +++ b/mastodon/src/main/res/layout/alt_badge.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mastodon/src/main/res/layout/compose_fab.xml b/mastodon/src/main/res/layout/compose_fab.xml new file mode 100644 index 000000000..e2017ac5c --- /dev/null +++ b/mastodon/src/main/res/layout/compose_fab.xml @@ -0,0 +1,5 @@ + + diff --git a/mastodon/src/main/res/layout/display_item_filter_warning.xml b/mastodon/src/main/res/layout/display_item_filter_warning.xml new file mode 100644 index 000000000..a2ac8bf86 --- /dev/null +++ b/mastodon/src/main/res/layout/display_item_filter_warning.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_footer.xml b/mastodon/src/main/res/layout/display_item_footer.xml index d3aa1c3db..99896e58f 100644 --- a/mastodon/src/main/res/layout/display_item_footer.xml +++ b/mastodon/src/main/res/layout/display_item_footer.xml @@ -4,8 +4,7 @@ android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="48dp" - android:paddingLeft="20dp" - android:paddingRight="20dp"> + android:paddingHorizontal="16dp"> @@ -103,14 +105,14 @@ + android:layout_height="match_parent"> diff --git a/mastodon/src/main/res/layout/display_item_gifv.xml b/mastodon/src/main/res/layout/display_item_gifv.xml index 4dccf3eff..a3575ab06 100644 --- a/mastodon/src/main/res/layout/display_item_gifv.xml +++ b/mastodon/src/main/res/layout/display_item_gifv.xml @@ -25,4 +25,6 @@ android:layout_margin="8dp" android:background="@drawable/ic_gif"/> + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_header.xml b/mastodon/src/main/res/layout/display_item_header.xml index fce17769a..48302daa5 100644 --- a/mastodon/src/main/res/layout/display_item_header.xml +++ b/mastodon/src/main/res/layout/display_item_header.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingTop="11dp" + android:paddingTop="13dp" android:paddingEnd="4dp" android:paddingStart="16dp"> @@ -41,11 +41,35 @@ android:src="@drawable/ic_visibility" android:tint="?android:textColorSecondary" /> + + + + + + + + + android:layout_marginTop="3dp" /> diff --git a/mastodon/src/main/res/layout/display_item_photo.xml b/mastodon/src/main/res/layout/display_item_photo.xml index d3129d379..462dab84a 100644 --- a/mastodon/src/main/res/layout/display_item_photo.xml +++ b/mastodon/src/main/res/layout/display_item_photo.xml @@ -1,6 +1,5 @@ @@ -11,81 +10,6 @@ android:layout_gravity="center" android:scaleType="centerCrop"/> - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_text.xml b/mastodon/src/main/res/layout/display_item_text.xml index 15907788e..6553146c9 100644 --- a/mastodon/src/main/res/layout/display_item_text.xml +++ b/mastodon/src/main/res/layout/display_item_text.xml @@ -44,19 +44,48 @@ android:background="?attr/colorPollVoted"/> - + android:requiresFadingEdge="vertical" + android:scrollbars="none" + android:fadingEdgeLength="36dp"> + + + + + + + +