Skip to content
This repository was archived by the owner on Oct 28, 2024. It is now read-only.

Commit

Permalink
reimplement thread ancestry
Browse files Browse the repository at this point in the history
  • Loading branch information
sk22 committed Jun 2, 2023
1 parent 02e3421 commit 56a9328
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,41 +19,40 @@ private Status fakeStatus(String id, String inReplyTo) {
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<Pair<String, Integer>> actual =
ThreadFragment.countAncestryLevels("main status", context);

List<Pair<String, Integer>> 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<ThreadFragment.NeighborAncestryInfo> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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;
Expand All @@ -26,7 +27,6 @@
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;
Expand All @@ -35,7 +35,6 @@
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;
Expand Down Expand Up @@ -207,7 +206,7 @@ public void setPhotoViewVisibility(int index, boolean visible){
@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};
Expand Down Expand Up @@ -339,6 +338,8 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
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){
Expand All @@ -350,23 +351,40 @@ public void getSelectorBounds(View view, Rect outRect){
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
holder=list.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder){
if(holder instanceof StatusDisplayItem.Holder<?> 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());
Expand Down Expand Up @@ -779,7 +797,7 @@ public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull Rec
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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
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;
Expand All @@ -17,6 +19,7 @@
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;
Expand All @@ -30,8 +33,10 @@
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;

Expand All @@ -55,6 +60,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
* confused? good. /j
*/
private final List<Pair<String, Integer>> levels = new ArrayList<>();
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();

@Override
public void onCreate(Bundle savedInstanceState){
Expand All @@ -74,23 +80,27 @@ protected List<StatusDisplayItem> buildDisplayItems(Status s){
// "what the fuck is a deque"? yes
// (it's just so the last-added item automatically comes first when looping over it)
Deque<Integer> deleteTheseItems = new ArrayDeque<>();
for(int i = 0; i < items.size(); i++){
StatusDisplayItem item = items.get(i);

Optional<Pair<String, Integer>> 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<StatusDisplayItem> 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);
}

Expand All @@ -101,7 +111,7 @@ else if(item instanceof FooterStatusDisplayItem footer)
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()));
}
Expand All @@ -117,7 +127,7 @@ public void onSuccess(StatusContext result){
if (getActivity() == null) return;
if(refreshing){
data.clear();
levels.clear();
ancestryMap.clear();
displayItems.clear();
data.add(mainStatus);
onAppendItems(Collections.singletonList(mainStatus));
Expand All @@ -129,7 +139,9 @@ public void onSuccess(StatusContext result){
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);
Expand All @@ -156,26 +168,35 @@ public void onSuccess(StatusContext result){
.exec(accountID);
}

public static List<Pair<String, Integer>> countAncestryLevels(String mainStatusID, StatusContext context) {
List<Pair<String, Integer>> levels = new ArrayList<>();
public static List<NeighborAncestryInfo> mapNeighborhoodAncestry(Status mainStatus, StatusContext context) {
List<NeighborAncestryInfo> 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<Status> statuses = new ArrayList<>(context.ancestors);
statuses.add(mainStatus);
statuses.addAll(context.descendants);

levels.add(Pair.create(mainStatusID, 0));
Map<String, Integer> levelPerStatus = new HashMap<>();
int count = statuses.size();
for (int index = 0; index < count; index++) {
Status current = statuses.get(index);
NeighborAncestryInfo item = new NeighborAncestryInfo(current);

// 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))));
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);
}

return levels;
return ancestry;
}

public static void sortStatusContext(Status mainStatus, StatusContext context) {
Expand Down Expand Up @@ -275,4 +296,47 @@ protected Filter.FilterContext getFilterContext() {
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<Status> getStatus() {
return Optional.ofNullable(status);
}

public Optional<Status> getDescendantNeighbor() {
return Optional.ofNullable(descendantNeighbor);
}

public Optional<Status> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,23 @@ public void onBind(FooterStatusDisplayItem item){
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();
}
Expand Down
Loading

0 comments on commit 56a9328

Please sign in to comment.