diff --git a/mastodon/src/androidTest/java/org/joinmastodon/android/fragments/ThreadFragmentTest.java b/mastodon/src/androidTest/java/org/joinmastodon/android/fragments/ThreadFragmentTest.java index da091656c..524aaed83 100644 --- a/mastodon/src/androidTest/java/org/joinmastodon/android/fragments/ThreadFragmentTest.java +++ b/mastodon/src/androidTest/java/org/joinmastodon/android/fragments/ThreadFragmentTest.java @@ -19,41 +19,40 @@ public class ThreadFragmentTest { return status; } + private ThreadFragment.NeighborAncestryInfo fakeInfo(Status s, Status d, Status a) { + ThreadFragment.NeighborAncestryInfo info = new ThreadFragment.NeighborAncestryInfo(s); + info.descendantNeighbor = d; + info.ancestoringNeighbor = a; + return info; + } + @Test - public void countAncestryLevels() { + public void mapNeighborhoodAncestry() { StatusContext context = new StatusContext(); context.ancestors = List.of( fakeStatus("oldest ancestor", null), fakeStatus("younger ancestor", "oldest ancestor") ); + Status mainStatus = fakeStatus("main status", "younger ancestor"); context.descendants = List.of( fakeStatus("first reply", "main status"), fakeStatus("reply to first reply", "first reply"), fakeStatus("third level reply", "reply to first reply"), fakeStatus("another reply", "main status") ); - List> actual = - ThreadFragment.countAncestryLevels("main status", context); - List> expected = List.of( - Pair.create("oldest ancestor", -2), - Pair.create("younger ancestor", -1), - Pair.create("main status", 0), - Pair.create("first reply", 1), - Pair.create("reply to first reply", 2), - Pair.create("third level reply", 3), - Pair.create("another reply", 1) - ); - assertEquals( - "status ids are in the right order", - expected.stream().map(p -> p.first).collect(Collectors.toList()), - actual.stream().map(p -> p.first).collect(Collectors.toList()) - ); - assertEquals( - "counted levels match", - expected.stream().map(p -> p.second).collect(Collectors.toList()), - actual.stream().map(p -> p.second).collect(Collectors.toList()) - ); + List neighbors = + ThreadFragment.mapNeighborhoodAncestry(mainStatus, context); + + assertEquals(List.of( + fakeInfo(context.ancestors.get(0), context.ancestors.get(1), null), + fakeInfo(context.ancestors.get(1), mainStatus, context.ancestors.get(0)), + fakeInfo(mainStatus, context.descendants.get(0), context.ancestors.get(1)), + fakeInfo(context.descendants.get(0), context.descendants.get(1), mainStatus), + fakeInfo(context.descendants.get(1), context.descendants.get(2), context.descendants.get(0)), + fakeInfo(context.descendants.get(2), null, context.descendants.get(1)), + fakeInfo(context.descendants.get(3), null, null) + ), neighbors); } @Test 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 fb0e495f2..9a636c3cf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -16,6 +16,7 @@ import android.text.TextPaint; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.animation.TranslateAnimation; import android.widget.ImageButton; @@ -26,7 +27,6 @@ import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.polls.SubmitPollVote; -import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.DisplayItemsParent; @@ -35,7 +35,6 @@ import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; @@ -207,7 +206,7 @@ public abstract class BaseStatusListFragment exten @Override public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ MediaAttachmentViewController holder=findPhotoViewHolder(index); - if(holder!=null){ + if(holder!=null && list!=null){ transitioningHolder=holder; View view=transitioningHolder.photo; int[] pos={0, 0}; @@ -339,6 +338,8 @@ public abstract class BaseStatusListFragment exten private Rect tmpRect=new Rect(); @Override public void getSelectorBounds(View view, Rect outRect){ + boolean hasDescendant = false, hasAncestor = false, isWarning = false; + int lastIndex = -1, firstIndex = -1; list.getDecoratedBoundsWithMargins(view, outRect); RecyclerView.ViewHolder holder=list.getChildViewHolder(view); if(holder instanceof StatusDisplayItem.Holder){ @@ -350,23 +351,40 @@ public abstract class BaseStatusListFragment exten for(int i=0;i h){ String otherID=((StatusDisplayItem.Holder) holder).getItemID(); if(otherID.equals(id)){ + if (firstIndex < 0) firstIndex = i; + lastIndex = i; + StatusDisplayItem item = h.getItem(); + hasDescendant = item.hasDescendantNeighbor(); + // no for direct descendants because main status (right above) is + // being displayed with an extended footer - no connected layout + hasAncestor = item.hasAncestoringNeighbor() && !item.isDirectDescendant; list.getDecoratedBoundsWithMargins(child, tmpRect); outRect.left=Math.min(outRect.left, tmpRect.left); outRect.top=Math.min(outRect.top, tmpRect.top); outRect.right=Math.max(outRect.right, tmpRect.right); - int bottom = tmpRect.bottom; - if (holder instanceof FooterStatusDisplayItem.Holder fh - && fh.getItem().hasDescendantSibling) { - bottom += V.dp(8); + outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom); + if (holder instanceof WarningFilteredStatusDisplayItem.Holder) { + isWarning = true; } - outRect.bottom=Math.max(outRect.bottom, bottom); } } } } + // shifting the selection box down + // see also: FooterStatusDisplayItem#onBind (setMargins) + if (isWarning || firstIndex < 0 || lastIndex < 0) return; + int prevIndex = firstIndex - 1, nextIndex = lastIndex + 1; + boolean prevIsWarning = prevIndex > 0 && prevIndex < list.getChildCount() && + list.getChildViewHolder(list.getChildAt(prevIndex)) + instanceof WarningFilteredStatusDisplayItem.Holder; + boolean nextIsWarning = nextIndex > 0 && nextIndex < list.getChildCount() && + list.getChildViewHolder(list.getChildAt(nextIndex)) + instanceof WarningFilteredStatusDisplayItem.Holder; + if (!prevIsWarning && hasAncestor) outRect.top += V.dp(4); + if (!nextIsWarning && hasDescendant) outRect.bottom += V.dp(4); } }); list.setItemAnimator(new BetterItemAnimator()); @@ -779,7 +797,7 @@ public abstract class BaseStatusListFragment exten RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling); if(holder instanceof StatusDisplayItem.Holder ih && siblingHolder instanceof StatusDisplayItem.Holder sh && (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){ - if (ih.getItem().descendantLevel != 0 && ih.getItem().hasDescendantSibling) continue; + if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor()) continue; drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint); } } 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 e7ce4717e..256af6401 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java @@ -5,6 +5,8 @@ import android.os.Bundle; import android.util.Pair; import android.view.View; +import androidx.annotation.NonNull; + import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.statuses.GetStatusContext; import org.joinmastodon.android.events.StatusCreatedEvent; @@ -17,6 +19,7 @@ import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem; import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.utils.ProvidesAssistContent; @@ -30,8 +33,10 @@ import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import me.grishka.appkit.api.SimpleCallback; @@ -55,6 +60,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist * confused? good. /j */ private final List> levels = new ArrayList<>(); + private final HashMap ancestryMap = new HashMap<>(); @Override public void onCreate(Bundle savedInstanceState){ @@ -74,23 +80,27 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist // "what the fuck is a deque"? yes // (it's just so the last-added item automatically comes first when looping over it) Deque deleteTheseItems = new ArrayDeque<>(); - for(int i = 0; i < items.size(); i++){ - StatusDisplayItem item = items.get(i); - Optional> levelForStatus = - levels.stream().filter(p -> p.first.equals(s.id)).findAny(); - item.descendantLevel = levelForStatus.map(p -> p.second).orElse(0); - if (levelForStatus.isPresent()) { - int idx = levels.indexOf(levelForStatus.get()); - item.hasDescendantSibling = (levels.size() > idx + 1) - && levels.get(idx + 1).second > levelForStatus.get().second; - item.isDescendantSibling = (idx - 1 >= 0) - && levels.get(idx - 1).second < levelForStatus.get().second; + // modifying hidden filtered items if status is displayed as a warning + List itemsToModify = + (items.get(0) instanceof WarningFilteredStatusDisplayItem warning) + ? warning.filteredItems + : items; + + for(int i = 0; i < itemsToModify.size(); i++){ + StatusDisplayItem item = itemsToModify.get(i); + NeighborAncestryInfo ancestryInfo = ancestryMap.get(s.id); + if (ancestryInfo != null) { + item.setAncestryInfo( + ancestryInfo, + s.id.equals(mainStatus.id), + ancestryInfo.getAncestoringNeighbor() + .map(ancestor -> ancestor.id.equals(mainStatus.id)) + .orElse(false) + ); } - if (item instanceof ReblogOrReplyLineStatusDisplayItem - && item.isDescendantSibling - && item.descendantLevel != 1) { + if (item instanceof ReblogOrReplyLineStatusDisplayItem && !item.isDirectDescendant) { deleteTheseItems.add(i); } @@ -101,7 +111,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist footer.hideCounts=true; } } - for (int deleteThisItem : deleteTheseItems) items.remove(deleteThisItem); + for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem); if(s.id.equals(mainStatus.id)) { items.add(new ExtendedFooterStatusDisplayItem(s.id, this, s.getContentStatus())); } @@ -117,7 +127,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist if (getActivity() == null) return; if(refreshing){ data.clear(); - levels.clear(); + ancestryMap.clear(); displayItems.clear(); data.add(mainStatus); onAppendItems(Collections.singletonList(mainStatus)); @@ -129,7 +139,9 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist result.descendants=filterStatuses(result.descendants); result.ancestors=filterStatuses(result.ancestors); - levels.addAll(countAncestryLevels(mainStatus.id, result)); + for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) { + ancestryMap.put(i.status.id, i); + } if(footerProgress!=null) footerProgress.setVisibility(View.GONE); @@ -156,26 +168,35 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist .exec(accountID); } - public static List> countAncestryLevels(String mainStatusID, StatusContext context) { - List> levels = new ArrayList<>(); + public static List mapNeighborhoodAncestry(Status mainStatus, StatusContext context) { + List ancestry = new ArrayList<>(); - for (int i = 0; i < context.ancestors.size(); i++) { - levels.add(Pair.create( - context.ancestors.get(i).id, - -context.ancestors.size() + i // -3, -2, -1 - )); + List statuses = new ArrayList<>(context.ancestors); + statuses.add(mainStatus); + statuses.addAll(context.descendants); + + int count = statuses.size(); + for (int index = 0; index < count; index++) { + Status current = statuses.get(index); + NeighborAncestryInfo item = new NeighborAncestryInfo(current); + + item.descendantNeighbor = Optional + .ofNullable(count > index + 1 ? statuses.get(index + 1) : null) + .filter(s -> s.inReplyToId.equals(current.id)) + .orElse(null); + + item.ancestoringNeighbor = Optional.ofNullable(index > 0 ? ancestry.get(index - 1) : null) + .filter(ancestor -> ancestor + .getDescendantNeighbor() + .map(ancestorsDescendant -> ancestorsDescendant.id.equals(current.id)) + .orElse(false)) + .flatMap(NeighborAncestryInfo::getStatus) + .orElse(null); + + ancestry.add(item); } - levels.add(Pair.create(mainStatusID, 0)); - Map levelPerStatus = new HashMap<>(); - - // sum up the amounts of descendants per status - context.descendants.forEach(s -> levelPerStatus.put(s.id, - levelPerStatus.getOrDefault(s.inReplyToId, 0) + 1)); - context.descendants.forEach(s -> - levels.add(Pair.create(s.id, levelPerStatus.get(s.id)))); - - return levels; + return ancestry; } public static void sortStatusContext(Status mainStatus, StatusContext context) { @@ -275,4 +296,47 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist public Uri getWebUri(Uri.Builder base) { return Uri.parse(mainStatus.url); } + + public static class NeighborAncestryInfo { + protected Status status, descendantNeighbor, ancestoringNeighbor; + + public NeighborAncestryInfo(@NonNull Status status) { + this.status = status; + } + + public Optional getStatus() { + return Optional.ofNullable(status); + } + + public Optional getDescendantNeighbor() { + return Optional.ofNullable(descendantNeighbor); + } + + public Optional getAncestoringNeighbor() { + return Optional.ofNullable(ancestoringNeighbor); + } + + public boolean hasDescendantNeighbor() { + return getDescendantNeighbor().isPresent(); + } + + public boolean hasAncestoringNeighbor() { + return getAncestoringNeighbor().isPresent(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NeighborAncestryInfo that = (NeighborAncestryInfo) o; + return status.equals(that.status) + && Objects.equals(descendantNeighbor, that.descendantNeighbor) + && Objects.equals(ancestoringNeighbor, that.ancestoringNeighbor); + } + + @Override + public int hashCode() { + return Objects.hash(status, descendantNeighbor, ancestoringNeighbor); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java index d4f84da2e..ad423b8a7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java @@ -136,16 +136,23 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ bindButton(favorite, item.status.favouritesCount); // in thread view, direct descendant posts display one direct reply to themselves, // hence in that case displaying whether there is another reply - reply.setSelected(item.status.repliesCount > (item.descendantLevel > 0 ? 1 : 0)); + int compareTo = item.isMainStatus || !item.hasDescendantNeighbor() ? 0 : 1; + reply.setSelected(item.status.repliesCount > compareTo); boost.setSelected(item.status.reblogged); favorite.setSelected(item.status.favourited); bookmark.setSelected(item.status.bookmarked); boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL || (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id))); + int nextPos = getAbsoluteAdapterPosition() + 1; + boolean nextIsWarning = item.parentFragment.getDisplayItems().size() > nextPos && + item.parentFragment.getDisplayItems().get(nextPos) instanceof WarningFilteredStatusDisplayItem; + boolean condenseBottom = !item.isMainStatus && item.hasDescendantNeighbor() && + !nextIsWarning; + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams(); params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, - item.descendantLevel != 0 && item.hasDescendantSibling ? V.dp(-6) : 0); + condenseBottom ? V.dp(-8) : 0); itemView.requestLayout(); } 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 edc3f6861..3d2e3e161 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 @@ -10,7 +10,6 @@ import android.view.ViewGroup; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment; @@ -22,7 +21,6 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.Filter; -import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.ScheduledStatus; @@ -49,8 +47,32 @@ public abstract class StatusDisplayItem{ public final BaseStatusListFragment parentFragment; public boolean inset; public int index; - public int descendantLevel; - public boolean hasDescendantSibling, isDescendantSibling; + private ThreadFragment.NeighborAncestryInfo ancestryInfo; + public boolean + isMainStatus = true, + isDirectDescendant = false; + + public boolean hasDescendantNeighbor() { + return Optional.ofNullable(ancestryInfo) + .map(ThreadFragment.NeighborAncestryInfo::hasDescendantNeighbor) + .orElse(false); + } + + public boolean hasAncestoringNeighbor() { + return Optional.ofNullable(ancestryInfo) + .map(ThreadFragment.NeighborAncestryInfo::hasAncestoringNeighbor) + .orElse(false); + } + + public void setAncestryInfo( + ThreadFragment.NeighborAncestryInfo ancestryInfo, + boolean isMainStatus, + boolean isDirectDescendant + ) { + this.ancestryInfo = ancestryInfo; + this.isMainStatus = isMainStatus; + this.isDirectDescendant = isDirectDescendant; + } public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){ this.parentID=parentID;