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 8322d042d..919e68349 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java
@@ -1,5 +1,8 @@
package org.joinmastodon.android.fragments;
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
@@ -90,12 +93,14 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
+import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
+import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
-public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{
+public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, CustomTransitionsFragment{
private static final int MEDIA_RESULT=717;
public static final int IMAGE_DESCRIPTION_RESULT=363;
@@ -1124,4 +1129,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void setPostLanguage(ComposeLanguageAlertViewController.SelectedOption language){
postLang=language;
}
+
+ @Override
+ public Animator onCreateEnterTransition(View prev, View container){
+ AnimatorSet anim=new AnimatorSet();
+ if(getArguments().getBoolean("fromThreadFragment")){
+ anim.playTogether(
+ ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
+ ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
+ );
+ }else{
+ anim.playTogether(
+ ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
+ ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
+ );
+ }
+ anim.setDuration(300);
+ anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
+ return anim;
+ }
+
+ @Override
+ public Animator onCreateExitTransition(View prev, View container){
+ AnimatorSet anim=new AnimatorSet();
+ anim.playTogether(
+ ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
+ ObjectAnimator.ofFloat(container, View.ALPHA, 0)
+ );
+ anim.setDuration(200);
+ anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
+ return anim;
+ }
}
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 6d274d9ae..c9c405c91 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java
@@ -2,19 +2,24 @@ package org.joinmastodon.android.fragments;
import android.content.res.ColorStateList;
import android.os.Bundle;
+import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
import org.joinmastodon.android.api.session.AccountSessionManager;
-import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
-import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
+import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
@@ -26,10 +31,12 @@ import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
+import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
+import me.grishka.appkit.imageloader.ViewImageLoader;
+import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
@@ -37,10 +44,16 @@ import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment{
private Status mainStatus;
private ImageView endMark;
+ private FrameLayout replyContainer;
+ private LinearLayout replyButton;
+ private ImageView replyButtonAva;
+ private TextView replyButtonText;
+ private int lastBottomInset;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
+ setLayout(R.layout.fragment_thread);
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
if(inReplyToAccount!=null)
@@ -126,6 +139,20 @@ public class ThreadFragment extends StatusListFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
+ replyContainer=view.findViewById(R.id.reply_button_wrapper);
+ replyButton=replyContainer.findViewById(R.id.reply_button);
+ replyButtonText=replyButton.findViewById(R.id.reply_btn_text);
+ replyButtonAva=replyButton.findViewById(R.id.avatar);
+ replyButton.setOutlineProvider(OutlineProviders.roundedRect(20));
+ replyButton.setClipToOutline(true);
+ replyButtonText.setText(getString(R.string.reply_to_user, mainStatus.account.displayName));
+ replyButtonAva.setOutlineProvider(OutlineProviders.OVAL);
+ replyButtonAva.setClipToOutline(true);
+ replyButton.setOnClickListener(v->openReply());
+ Account self=AccountSessionManager.get(accountID).self;
+ if(!TextUtils.isEmpty(self.avatar)){
+ ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
+ }
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
showContent();
if(!loaded)
@@ -175,4 +202,22 @@ public class ThreadFragment extends StatusListFragment{
}
super.onErrorRetryClick();
}
+
+ @Override
+ public void onApplyWindowInsets(WindowInsets insets){
+ lastBottomInset=insets.getSystemWindowInsetBottom();
+ super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
+ }
+
+ private void openReply(){
+ Bundle args=new Bundle();
+ args.putString("account", accountID);
+ args.putParcelable("replyTo", Parcels.wrap(mainStatus));
+ args.putBoolean("fromThreadFragment", true);
+ Nav.go(getActivity(), ComposeFragment.class, args);
+ }
+
+ public int getSnackbarOffset(){
+ return replyContainer.getHeight()-lastBottomInset;
+ }
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/Snackbar.java b/mastodon/src/main/java/org/joinmastodon/android/ui/Snackbar.java
new file mode 100644
index 000000000..7ee499756
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/Snackbar.java
@@ -0,0 +1,217 @@
+package org.joinmastodon.android.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Outline;
+import android.graphics.PixelFormat;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.ui.utils.UiUtils;
+
+import androidx.annotation.Keep;
+import androidx.annotation.StringRes;
+import me.grishka.appkit.utils.CubicBezierInterpolator;
+import me.grishka.appkit.utils.V;
+
+public class Snackbar{
+ private static Snackbar current;
+
+ private final Context context;
+ private int bottomOffset;
+ private FrameLayout windowView;
+ private LinearLayout contentView;
+ private boolean hasAction;
+ private AnimatableOutlineProvider outlineProvider;
+ private Animator currentAnim;
+ private Runnable dismissRunnable=this::dismiss;
+
+ private Snackbar(Context context, String text, String action, Runnable onActionClick, int bottomOffset){
+ this.context=context;
+ this.bottomOffset=bottomOffset;
+ hasAction=onActionClick!=null;
+
+ windowView=new FrameLayout(context);
+ windowView.setClipToPadding(false);
+ contentView=new LinearLayout(context);
+ contentView.setOrientation(LinearLayout.HORIZONTAL);
+ contentView.setBaselineAligned(false);
+ contentView.setBackgroundColor(UiUtils.getThemeColor(context, R.attr.colorM3SurfaceInverse));
+ contentView.setOutlineProvider(outlineProvider=new AnimatableOutlineProvider(contentView));
+ contentView.setClipToOutline(true);
+ contentView.setElevation(V.dp(6));
+ contentView.setPaddingRelative(V.dp(16), 0, V.dp(8), 0);
+ FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp.leftMargin=lp.topMargin=lp.rightMargin=lp.bottomMargin=V.dp(16);
+ windowView.addView(contentView, lp);
+
+ TextView textView=new TextView(context);
+ textView.setTextAppearance(R.style.m3_body_medium);
+ textView.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceInverse));
+ textView.setMaxLines(2);
+ textView.setEllipsize(TextUtils.TruncateAt.END);
+ textView.setText(text);
+ textView.setMinHeight(V.dp(48));
+ textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
+ textView.setPadding(0, V.dp(14), 0, V.dp(14));
+ contentView.addView(textView, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
+
+ if(action!=null){
+ Button button=new Button(context);
+ int primaryInverse=UiUtils.getThemeColor(context, R.attr.colorM3PrimaryInverse);
+ button.setTextColor(primaryInverse);
+ button.setBackgroundResource(R.drawable.bg_rect_4dp_ripple);
+ button.setBackgroundTintList(ColorStateList.valueOf(primaryInverse));
+ button.setText(action);
+ button.setPadding(V.dp(8), 0, V.dp(8), 0);
+ button.setOnClickListener(v->onActionClick.run());
+ LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(40));
+ blp.leftMargin=blp.topMargin=blp.rightMargin=blp.bottomMargin=V.dp(4);
+ contentView.addView(button, blp);
+ }
+ }
+
+ public void show(){
+ if(current!=null)
+ current.dismiss();
+ current=this;
+ WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
+ lp.width=ViewGroup.LayoutParams.MATCH_PARENT;
+ lp.height=ViewGroup.LayoutParams.WRAP_CONTENT;
+ lp.gravity=Gravity.BOTTOM;
+ lp.y=bottomOffset;
+ WindowManager wm=context.getSystemService(WindowManager.class);
+ wm.addView(windowView, lp);
+ windowView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
+ @Override
+ public boolean onPreDraw(){
+ windowView.getViewTreeObserver().removeOnPreDrawListener(this);
+
+ AnimatorSet set=new AnimatorSet();
+ set.playTogether(
+ ObjectAnimator.ofFloat(outlineProvider, "fraction", 0, 1),
+ ObjectAnimator.ofFloat(contentView, View.ALPHA, 0, 1)
+ );
+ set.setInterpolator(AnimationUtils.loadInterpolator(context, R.interpolator.m3_sys_motion_easing_standard_decelerate));
+ set.setDuration(350);
+ set.addListener(new AnimatorListenerAdapter(){
+ @Override
+ public void onAnimationEnd(Animator animation){
+ currentAnim=null;
+ }
+ });
+ currentAnim=set;
+ set.start();
+
+ return true;
+ }
+ });
+ windowView.postDelayed(dismissRunnable, 4000);
+ }
+
+ public void dismiss(){
+ current=null;
+ if(currentAnim!=null){
+ currentAnim.cancel();
+ }
+ windowView.removeCallbacks(dismissRunnable);
+ ObjectAnimator anim=ObjectAnimator.ofFloat(contentView, View.ALPHA, 0);
+ anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
+ anim.setDuration(200);
+ anim.addListener(new AnimatorListenerAdapter(){
+ @Override
+ public void onAnimationEnd(Animator animation){
+ WindowManager wm=context.getSystemService(WindowManager.class);
+ wm.removeView(windowView);
+ }
+ });
+ anim.start();
+ }
+
+ private static class AnimatableOutlineProvider extends ViewOutlineProvider{
+ private float fraction=1f;
+ private final View view;
+
+ private AnimatableOutlineProvider(View view){
+ this.view=view;
+ }
+
+ @Override
+ public void getOutline(View view, Outline outline){
+ outline.setRoundRect(0, Math.round(view.getHeight()*(1f-fraction)), view.getWidth(), view.getHeight(), V.dp(4));
+ }
+
+ @Keep
+ public float getFraction(){
+ return fraction;
+ }
+
+ @Keep
+ public void setFraction(float fraction){
+ this.fraction=fraction;
+ view.invalidateOutline();
+ }
+ }
+
+ public static class Builder{
+ private final Context context;
+ private String text;
+ private String action;
+ private Runnable onActionClick;
+ private int bottomOffset;
+
+ public Builder(Context context){
+ this.context=context;
+ }
+
+ public Builder setText(String text){
+ this.text=text;
+ return this;
+ }
+
+ public Builder setText(@StringRes int res){
+ text=context.getString(res);
+ return this;
+ }
+
+ public Builder setAction(String action, Runnable onActionClick){
+ this.action=action;
+ this.onActionClick=onActionClick;
+ return this;
+ }
+
+ public Builder setAction(@StringRes int action, Runnable onActionClick){
+ this.action=context.getString(action);
+ this.onActionClick=onActionClick;
+ return this;
+ }
+
+ public Builder setBottomOffset(int offset){
+ bottomOffset=offset;
+ return this;
+ }
+
+ public Snackbar create(){
+ return new Snackbar(context, text, action, onActionClick, bottomOffset);
+ }
+
+ public void show(){
+ create().show();
+ }
+ }
+}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java
index 417ce148c..6dd1db607 100644
--- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java
+++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java
@@ -13,14 +13,17 @@ import android.text.style.TypefaceSpan;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.StatusEditHistoryFragment;
+import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragment;
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
import org.joinmastodon.android.model.Status;
+import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
@@ -34,11 +37,13 @@ import java.util.Locale;
import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes;
import me.grishka.appkit.Nav;
+import me.grishka.appkit.utils.V;
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
public final Status status;
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
+ private static final DateTimeFormatter TIME_FORMATTER_LONG=DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
private static final DateTimeFormatter DATE_FORMATTER=DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
public ExtendedFooterStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
@@ -160,7 +165,14 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
}
private void showTimeSnackbar(){
-
+ int bottomOffset=0;
+ if(item.parentFragment instanceof ThreadFragment tf){
+ bottomOffset=tf.getSnackbarOffset();
+ }
+ new Snackbar.Builder(itemView.getContext())
+ .setText(itemView.getContext().getString(R.string.posted_at, TIME_FORMATTER_LONG.format(item.status.createdAt.atZone(ZoneId.systemDefault()))))
+ .setBottomOffset(bottomOffset)
+ .show();
}
}
}
diff --git a/mastodon/src/main/res/drawable/bg_rect_ripple.xml b/mastodon/src/main/res/drawable/bg_rect_ripple.xml
new file mode 100644
index 000000000..a6322aabf
--- /dev/null
+++ b/mastodon/src/main/res/drawable/bg_rect_ripple.xml
@@ -0,0 +1,8 @@
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/layout/fragment_thread.xml b/mastodon/src/main/res/layout/fragment_thread.xml
new file mode 100644
index 000000000..0d817d825
--- /dev/null
+++ b/mastodon/src/main/res/layout/fragment_thread.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml
index 74b745083..6153f9ab9 100644
--- a/mastodon/src/main/res/values/attrs.xml
+++ b/mastodon/src/main/res/values/attrs.xml
@@ -15,7 +15,9 @@
+
+
diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml
index 6d77ae5e7..1f8859468 100644
--- a/mastodon/src/main/res/values/strings.xml
+++ b/mastodon/src/main/res/values/strings.xml
@@ -656,4 +656,6 @@
Manage list members
No members yet
Find users to add
+ Reply to %s
+ Posted at %s
\ No newline at end of file
diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml
index b48628e0c..fb39928cb 100644
--- a/mastodon/src/main/res/values/styles.xml
+++ b/mastodon/src/main/res/values/styles.xml
@@ -53,6 +53,8 @@
- #F9DEDC
- #410E0B
- @color/m3_sys_dark_primary
+ - @color/m3_sys_dark_surface
+ - @color/m3_sys_dark_on_surface
- #FFF
- #8b5000
- #ab332a
@@ -120,6 +122,8 @@
- #8C1D18
- #F9DEDC
- @color/m3_sys_light_primary
+ - @color/m3_sys_light_surface
+ - @color/m3_sys_light_on_surface
- #000
- #ffb871
- #ffb4aa