implement bidirectional missing posts gap

This commit is contained in:
sk
2023-09-08 21:16:42 +02:00
parent de5165434d
commit efa003a9a5
17 changed files with 191 additions and 117 deletions

View File

@@ -1,5 +1,8 @@
package org.joinmastodon.android.ui.displayitems;
import static org.joinmastodon.android.ui.utils.UiUtils.opacityIn;
import static org.joinmastodon.android.ui.utils.UiUtils.opacityOut;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
@@ -12,8 +15,6 @@ import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -56,7 +57,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView replies, boosts, favorites;
private final View reply, boost, favorite, share, bookmark;
private static final Animation opacityOut, opacityIn;
private View touchingView = null;
private boolean longClickPerformed = false;
@@ -77,18 +77,6 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
};
private static final float ALPHA_PRESSED=0.55f;
static {
opacityOut = new AlphaAnimation(1, ALPHA_PRESSED);
opacityOut.setDuration(300);
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
opacityOut.setFillAfter(true);
opacityIn = new AlphaAnimation(ALPHA_PRESSED, 1);
opacityIn.setDuration(400);
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
}
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_footer, parent);

View File

@@ -8,14 +8,21 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.Instant;
import me.grishka.appkit.utils.V;
// Mind the gap!
public class GapStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
private Status status;
public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
public GapStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status){
super(parentID, parentFragment);
this.status=status;
}
@Override
@@ -24,25 +31,53 @@ public class GapStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<GapStatusDisplayItem>{
public final ProgressBar progress;
public final TextView text;
public final ProgressBar progressTop, progressBottom;
public final TextView textTop, gap, textBottom;
public final View top, bottom;
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_gap, parent);
progress=findViewById(R.id.progress);
text=findViewById(R.id.text);
itemView.setForeground(new SawtoothTearDrawable(context));
progressTop=findViewById(R.id.progress_top);
progressBottom=findViewById(R.id.progress_bottom);
textTop=findViewById(R.id.text_top);
textBottom=findViewById(R.id.text_bottom);
top=findViewById(R.id.top);
top.setOnClickListener(this::onViewClick);
bottom=findViewById(R.id.bottom);
bottom.setOnClickListener(this::onViewClick);
gap=findViewById(R.id.gap);
gap.setForeground(new SawtoothTearDrawable(context));
}
@Override
public void onBind(GapStatusDisplayItem item){
text.setVisibility(item.loading ? View.GONE : View.VISIBLE);
progress.setVisibility(item.loading ? View.VISIBLE : View.GONE);
if(!item.loading){
progressBottom.setVisibility(View.GONE);
progressTop.setVisibility(View.GONE);
}
top.setClickable(!item.loading);
bottom.setClickable(!item.loading);
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
Instant dateBelow=next instanceof HeaderStatusDisplayItem h ? h.status.createdAt
: next instanceof ReblogOrReplyLineStatusDisplayItem l ? l.status.createdAt
: null;
String text=dateBelow!=null && item.status.createdAt!=null && dateBelow.isBefore(item.status.createdAt)
? UiUtils.formatPeriodBetween(item.parentFragment.getContext(), dateBelow, item.status.createdAt)
: null;
gap.setText(text);
int p=text==null ? V.dp(6) : V.dp(20);
gap.setPadding(p, p, p, p);
}
private void onViewClick(View v){
if(item.loading) return;
boolean isTop=v==top;
(isTop ? textTop : textBottom).startAnimation(UiUtils.opacityOut);
V.setVisibilityAnimated((isTop ? progressTop : progressBottom), View.VISIBLE);
item.parentFragment.onGapClick(this, isTop);
}
@Override
public void onClick(){
item.parentFragment.onGapClick(this);
}
public void onClick(){}
}
}

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
@@ -43,18 +44,20 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
public boolean needBottomPadding;
ReblogOrReplyLineStatusDisplayItem extra;
CharSequence fullText;
Status status;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text);
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) {
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status);
}
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, CharSequence fullText) {
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, CharSequence fullText, Status status) {
super(parentID, parentFragment);
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
HtmlParser.parseCustomEmoji(ssb, emojis);
this.text=ssb;
emojiHelper.setText(ssb);
this.icon=icon;
this.status=status;
this.handleClick=handleClick;
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);

View File

@@ -135,7 +135,7 @@ public abstract class StatusDisplayItem{
: fragment.getString(R.string.in_reply_to, account.displayName);
return new ReblogOrReplyLineStatusDisplayItem(
parentID, fragment, text, account == null ? List.of() : account.emojis,
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText
R.drawable.ic_fluent_arrow_reply_20sp_filled, null, null, fullText, status
);
}
@@ -167,7 +167,7 @@ public abstract class StatusDisplayItem{
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
}, fullText));
}, fullText, status));
} else if (!(status.tags.isEmpty() ||
fragment instanceof HashtagTimelineFragment ||
fragment instanceof ListTimelineFragment
@@ -183,7 +183,7 @@ public abstract class StatusDisplayItem{
i -> {
args.putString("hashtag", hashtag.name);
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
}
}, status
)));
}
@@ -294,7 +294,7 @@ public abstract class StatusDisplayItem{
footer.hideCounts=hideCounts;
items.add(footer);
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
items.add(new GapStatusDisplayItem(parentID, fragment));
items.add(new GapStatusDisplayItem(parentID, fragment, status));
}
int i=1;
boolean inset=(flags & FLAG_INSET)!=0;

View File

@@ -23,9 +23,9 @@ import me.grishka.appkit.utils.V;
public class SawtoothTearDrawable extends Drawable{
private final Paint topPaint, bottomPaint;
private static final int TOP_SAWTOOTH_HEIGHT=5;
private static final int BOTTOM_SAWTOOTH_HEIGHT=3;
private static final int STROKE_WIDTH=2;
private static final int TOP_SAWTOOTH_HEIGHT=4;
private static final int BOTTOM_SAWTOOTH_HEIGHT=4;
private static final int STROKE_WIDTH=1;
private static final int SAWTOOTH_PERIOD=14;
public SawtoothTearDrawable(Context context){

View File

@@ -54,6 +54,8 @@ import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.webkit.MimeTypeMap;
import android.widget.Button;
import android.widget.ImageView;
@@ -96,7 +98,6 @@ import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerAboutFragment;
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
@@ -173,6 +174,19 @@ public class UiUtils {
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
public static int MAX_WIDTH, SCROLL_TO_TOP_DELTA;
public static final float ALPHA_PRESSED=0.55f;
public static final Animation opacityOut, opacityIn;
static {
opacityOut = new AlphaAnimation(1, ALPHA_PRESSED);
opacityOut.setDuration(300);
opacityOut.setInterpolator(CubicBezierInterpolator.DEFAULT);
opacityOut.setFillAfter(true);
opacityIn = new AlphaAnimation(ALPHA_PRESSED, 1);
opacityIn.setDuration(400);
opacityIn.setInterpolator(CubicBezierInterpolator.DEFAULT);
}
private UiUtils() {
}
@@ -192,28 +206,33 @@ public class UiUtils {
}
public static String formatRelativeTimestamp(Context context, Instant instant) {
long t = instant.toEpochMilli();
long now = System.currentTimeMillis();
return formatPeriodBetween(context, instant, null);
}
public static String formatPeriodBetween(Context context, Instant since, Instant until) {
boolean ago = until == null;
long t = since.toEpochMilli();
long now = ago ? System.currentTimeMillis() : until.toEpochMilli();
long diff = now - t;
if(diff<1000L){
return context.getString(R.string.time_now);
}else if(diff<60_000L){
return context.getString(R.string.time_seconds_ago_short, diff/1000L);
return context.getString(ago ? R.string.time_seconds_ago_short : R.string.sk_time_seconds, diff/1000L);
}else if(diff<3600_000L){
return context.getString(R.string.time_minutes_ago_short, diff/60_000L);
return context.getString(ago ? R.string.time_minutes_ago_short : R.string.sk_time_minutes, diff/60_000L);
}else if(diff<3600_000L*24L){
return context.getString(R.string.time_hours_ago_short, diff/3600_000L);
return context.getString(ago ? R.string.time_hours_ago_short : R.string.sk_time_hours, diff/3600_000L);
} else {
int days = (int) (diff / (3600_000L * 24L));
if (days > 30) {
ZonedDateTime dt = instant.atZone(ZoneId.systemDefault());
if (ago && days > 30) {
ZonedDateTime dt = since.atZone(ZoneId.systemDefault());
if (dt.getYear() == ZonedDateTime.now().getYear()) {
return DATE_FORMATTER_SHORT.format(dt);
} else {
return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
}
}
return context.getString(R.string.time_days_ago_short, days);
return context.getString(ago ? R.string.time_days_ago_short : R.string.sk_time_days, days);
}
}