From a082a3d325246f5857fdf5d99e9772f42120cef0 Mon Sep 17 00:00:00 2001 From: LucasGGamerM Date: Sun, 19 May 2024 10:20:15 -0300 Subject: [PATCH] feat(crash-status-display-item): makes the app not crash when creating the status display items This is the first part of a 2 part patch, because crashes can still happpen when the item is being "binded", which makes it necessary to handle those errors as well, which we currently DON'T do. Also the Error item is still needing to be better, so there is also that to work on --- .../displayitems/ErrorStatusDisplayItem.java | 50 +++ .../ui/displayitems/StatusDisplayItem.java | 378 +++++++++--------- 2 files changed, 242 insertions(+), 186 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ErrorStatusDisplayItem.java diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ErrorStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ErrorStatusDisplayItem.java new file mode 100644 index 000000000..7e8fea609 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ErrorStatusDisplayItem.java @@ -0,0 +1,50 @@ +package org.joinmastodon.android.ui.displayitems; + +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.ui.utils.UiUtils; + +public class ErrorStatusDisplayItem extends StatusDisplayItem{ + private final Exception exception; + + public ErrorStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Exception exception) { + super(parentID, parentFragment); + this.exception=exception; + } + + @Override + public Type getType() { + return Type.ERROR_ITEM; + } + + public static class Holder extends StatusDisplayItem.Holder { + private final TextView title, domain; + + public Holder(Context context, ViewGroup parent) { + super(context, R.layout.display_item_file, parent); + title=findViewById(R.id.title); + domain=findViewById(R.id.domain); + findViewById(R.id.inner).setOnClickListener(this::onClick); + } + + @Override + public void onBind(ErrorStatusDisplayItem item) { + title.setText(item.exception.getMessage()); +// title.setEllipsize(item.attachment.description != null ? TextUtils.TruncateAt.END : TextUtils.TruncateAt.MIDDLE); +// domain.setText(url.getHost()); + } + + private void onClick(View v) { +// UiUtils.openURL(itemView.getContext(), item.parentFragment.getAccountID(), getUrl()); + } + } +} + 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 9ac9d86b4..f6b6f5ca4 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 @@ -139,6 +139,7 @@ public abstract class StatusDisplayItem{ case SPOILER, FILTER_SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent, type); case SECTION_HEADER -> null; // new SectionHeaderStatusDisplayItem.Holder(activity, parent); case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent); + case ERROR_ITEM -> new ErrorStatusDisplayItem.Holder(activity, parent); case DUMMY -> new DummyStatusDisplayItem.Holder(activity); }; } @@ -164,201 +165,192 @@ public abstract class StatusDisplayItem{ Status statusForContent=status.getContentStatus(); Bundle args=new Bundle(); args.putString("account", accountID); - ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus s ? s : null; + try{ + ScheduledStatus scheduledStatus=parentObject instanceof ScheduledStatus s ? s : null; - HeaderStatusDisplayItem header=null; - boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts; + HeaderStatusDisplayItem header=null; + boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts; - if((flags & FLAG_NO_HEADER)==0){ - ReblogOrReplyLineStatusDisplayItem replyLine = null; - boolean threadReply = statusForContent.inReplyToAccountId != null && - statusForContent.inReplyToAccountId.equals(statusForContent.account.id); + if((flags&FLAG_NO_HEADER)==0){ + ReblogOrReplyLineStatusDisplayItem replyLine=null; + boolean threadReply=statusForContent.inReplyToAccountId!=null && + statusForContent.inReplyToAccountId.equals(statusForContent.account.id); - if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){ - Account account = knownAccounts.get(statusForContent.inReplyToAccountId); - replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply); + if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){ + Account account=knownAccounts.get(statusForContent.inReplyToAccountId); + replyLine=buildReplyLine(fragment, status, accountID, parentObject, account, threadReply); + } + + if(status.reblog!=null){ + boolean isOwnPost=AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account); + + statusForContent.rebloggedBy=status.account; + + String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName()); + 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); + }, null, status, status.account)); + }else if(!(status.tags.isEmpty() || + fragment instanceof HashtagTimelineFragment || + fragment instanceof ListTimelineFragment + ) && fragment.getParentFragment() instanceof HomeTabFragment home){ + home.getHashtags().stream() + .filter(followed->status.tags.stream() + .anyMatch(hashtag->followed.name.equalsIgnoreCase(hashtag.name))) + .findAny() + // post contains a hashtag the user is following + .ifPresent(hashtag->items.add(new ReblogOrReplyLineStatusDisplayItem( + parentID, fragment, hashtag.name, List.of(), + R.drawable.ic_fluent_number_symbol_20sp_filled, null, + i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag), + status + ))); + } + + if(replyLine!=null){ + Optional primaryLine=items.stream() + .filter(i->i instanceof ReblogOrReplyLineStatusDisplayItem) + .map(ReblogOrReplyLineStatusDisplayItem.class::cast) + .findFirst(); + + if(primaryLine.isPresent()){ + primaryLine.get().extra=replyLine; + }else{ + items.add(replyLine); + } + } + + if((flags&FLAG_CHECKABLE)!=0) + items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null)); + else + items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus)); } - if(status.reblog!=null){ - boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account); + LegacyFilter applyingFilter=null; + if(status.filtered!=null){ + List filters=status.filtered; - statusForContent.rebloggedBy = status.account; + // Only add client filters if there are no pre-existing status filter + if(filters.isEmpty()) + filters.addAll(AccountSessionManager.get(accountID).getClientSideFilters(status)); - String text=fragment.getString(R.string.user_boosted, status.account.getDisplayName()); - 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); - }, null, status, status.account)); - } else if (!(status.tags.isEmpty() || - fragment instanceof HashtagTimelineFragment || - fragment instanceof ListTimelineFragment - ) && fragment.getParentFragment() instanceof HomeTabFragment home) { - home.getHashtags().stream() - .filter(followed -> status.tags.stream() - .anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name))) - .findAny() - // post contains a hashtag the user is following - .ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem( - parentID, fragment, hashtag.name, List.of(), - R.drawable.ic_fluent_number_symbol_20sp_filled, null, - i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag), - status - ))); - } - - if (replyLine != null) { - Optional primaryLine = items.stream() - .filter(i -> i instanceof ReblogOrReplyLineStatusDisplayItem) - .map(ReblogOrReplyLineStatusDisplayItem.class::cast) - .findFirst(); - - if (primaryLine.isPresent()) { - primaryLine.get().extra = replyLine; - } else { - items.add(replyLine); + for(FilterResult filter : filters){ + LegacyFilter f=filter.filter; + if(f.isActive() && filterContext!=null && f.context.contains(filterContext)){ + applyingFilter=f; + break; + } } } - if((flags & FLAG_CHECKABLE)!=0) - items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null)); - else - items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, parentObject instanceof Notification n ? n : null, scheduledStatus)); - } - - LegacyFilter applyingFilter=null; - if(status.filtered!=null){ - List filters = status.filtered; - - // Only add client filters if there are no pre-existing status filter - if(filters.isEmpty()) - filters.addAll(AccountSessionManager.get(accountID).getClientSideFilters(status)); - - for(FilterResult filter:filters){ - LegacyFilter f=filter.filter; - if(f.isActive() && filterContext != null && f.context.contains(filterContext)){ - applyingFilter=f; - break; + ArrayList contentItems; + if(statusForContent.hasSpoiler()){ + if(AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed=true; + SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER); + if((flags&FLAG_IS_FOR_QUOTE)!=0){ + for(StatusDisplayItem item : spoilerItem.contentItems){ + item.isForQuote=true; + } } + items.add(spoilerItem); + contentItems=spoilerItem.contentItems; + }else{ + contentItems=items; } - } - ArrayList contentItems; - if(statusForContent.hasSpoiler()){ - if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true; - SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER); - if((flags & FLAG_IS_FOR_QUOTE)!=0){ - for(StatusDisplayItem item:spoilerItem.contentItems){ - item.isForQuote=true; - } + if(statusForContent.quote!=null){ + int quoteInlineIndex=statusForContent.content.lastIndexOf("

RE:"); + if(quoteInlineIndex!=-1) + statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex); } - items.add(spoilerItem); - contentItems=spoilerItem.contentItems; - }else{ - contentItems=items; - } - if(statusForContent.quote!=null) { - int quoteInlineIndex=statusForContent.content.lastIndexOf("

RE:"); - if (quoteInlineIndex!=-1) - statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex); - } - - boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText); - if(!TextUtils.isEmpty(statusForContent.content)){ - SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()); - if(applyingFilter!=null) - HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered); - TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, parsedText, fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0); - contentItems.add(text); - }else if(!hasSpoiler && header!=null){ - header.needBottomPadding=true; - }else if(hasSpoiler){ - contentItems.add(new DummyStatusDisplayItem(parentID, fragment)); - } - - List imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); - if(!imageAttachments.isEmpty() && (flags & FLAG_NO_MEDIA_PREVIEW)==0){ - int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorM3SurfaceVariant); - for (Attachment att : imageAttachments) { - if (att.blurhashPlaceholder == null) { - att.blurhashPlaceholder = new ColorDrawable(color); - } - } - PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments); - MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent); - if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){ - mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden); - statusForContent.sensitiveRevealed=false; - statusForContent.sensitive=true; - } else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia) - statusForContent.sensitiveRevealed=true; - contentItems.add(mediaGrid); - } - if((flags & FLAG_NO_MEDIA_PREVIEW)!=0){ - contentItems.add(new PreviewlessMediaGridStatusDisplayItem(parentID, fragment, null, imageAttachments, statusForContent)); - - } - for(Attachment att:statusForContent.mediaAttachments){ - if(att.type==Attachment.Type.AUDIO){ - contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att)); - } - if(att.type==Attachment.Type.UNKNOWN){ - contentItems.add(new FileStatusDisplayItem(parentID, fragment, att)); - } - } - if(statusForContent.poll!=null){ - buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems); - } - if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null && !statusForContent.card.isHashtagUrl(statusForContent.url)){ - contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags & FLAG_NO_MEDIA_PREVIEW)==0)); - } - if(statusForContent.quote!=null && !(parentObject instanceof Notification)){ - if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment + boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText); + if(!TextUtils.isEmpty(statusForContent.content)){ + SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()); + if(applyingFilter!=null) + HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered); + TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, parsedText, fragment, statusForContent, (flags&FLAG_NO_TRANSLATE)!=0); + contentItems.add(text); + }else if(!hasSpoiler && header!=null){ + header.needBottomPadding=true; + }else if(hasSpoiler){ contentItems.add(new DummyStatusDisplayItem(parentID, fragment)); - contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE)); - } - if(contentItems!=items && statusForContent.spoilerRevealed){ - items.addAll(contentItems); - } - AccountLocalPreferences lp=fragment.getLocalPrefs(); - if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled && - (lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) && - statusForContent.reactions!=null){ - boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id); - boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus; - items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false)); - } - FooterStatusDisplayItem footer=null; - if((flags & FLAG_NO_FOOTER)==0){ - footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID); - footer.hideCounts=hideCounts; - items.add(footer); - } - boolean inset=(flags & FLAG_INSET)!=0; - boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0; - // add inset dummy so last content item doesn't clip out of inset bounds - if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){ - items.add(new DummyStatusDisplayItem(parentID, fragment)); - // in case we ever need the dummy to display a margin for the media grid again: - // (i forgot why we apparently don't need this anymore) - // !contentItems.isEmpty() && contentItems - // .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem)); - } - GapStatusDisplayItem gap=null; - if((flags & FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment)) - items.add(gap=new GapStatusDisplayItem(parentID, fragment, status)); - int i=1; - for(StatusDisplayItem item:items){ - if(inset) - item.inset=true; - if(isForQuote){ - item.status=statusForContent; - item.isForQuote=true; } - item.index=i++; - } - if(items!=contentItems && !statusForContent.spoilerRevealed){ - for(StatusDisplayItem item:contentItems){ + + List imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); + if(!imageAttachments.isEmpty() && (flags&FLAG_NO_MEDIA_PREVIEW)==0){ + int color=UiUtils.getThemeColor(fragment.getContext(), R.attr.colorM3SurfaceVariant); + for(Attachment att : imageAttachments){ + if(att.blurhashPlaceholder==null){ + att.blurhashPlaceholder=new ColorDrawable(color); + } + } + PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments); + MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent); + if((flags&FLAG_MEDIA_FORCE_HIDDEN)!=0){ + mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden); + statusForContent.sensitiveRevealed=false; + statusForContent.sensitive=true; + }else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia) + statusForContent.sensitiveRevealed=true; + contentItems.add(mediaGrid); + } + if((flags&FLAG_NO_MEDIA_PREVIEW)!=0){ + contentItems.add(new PreviewlessMediaGridStatusDisplayItem(parentID, fragment, null, imageAttachments, statusForContent)); + + } + for(Attachment att : statusForContent.mediaAttachments){ + if(att.type==Attachment.Type.AUDIO){ + contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att)); + } + if(att.type==Attachment.Type.UNKNOWN){ + contentItems.add(new FileStatusDisplayItem(parentID, fragment, att)); + } + } + if(statusForContent.poll!=null){ + buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems); + } + if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null && !statusForContent.card.isHashtagUrl(statusForContent.url)){ + contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent, (flags&FLAG_NO_MEDIA_PREVIEW)==0)); + } + if(statusForContent.quote!=null && !(parentObject instanceof Notification)){ + if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment + contentItems.add(new DummyStatusDisplayItem(parentID, fragment)); + contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER|FLAG_INSET|FLAG_NO_EMOJI_REACTIONS|FLAG_IS_FOR_QUOTE)); + } + if(contentItems!=items && statusForContent.spoilerRevealed){ + items.addAll(contentItems); + } + AccountLocalPreferences lp=fragment.getLocalPrefs(); + if((flags&FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled && + (lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) && + statusForContent.reactions!=null){ + boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id); + boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus; + items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false)); + } + FooterStatusDisplayItem footer=null; + if((flags&FLAG_NO_FOOTER)==0){ + footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID); + footer.hideCounts=hideCounts; + items.add(footer); + } + boolean inset=(flags&FLAG_INSET)!=0; + boolean isForQuote=(flags&FLAG_IS_FOR_QUOTE)!=0; + // add inset dummy so last content item doesn't clip out of inset bounds + if((inset || footer==null) && (flags&FLAG_CHECKABLE)==0 && !isForQuote){ + items.add(new DummyStatusDisplayItem(parentID, fragment)); + // in case we ever need the dummy to display a margin for the media grid again: + // (i forgot why we apparently don't need this anymore) + // !contentItems.isEmpty() && contentItems + // .get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem)); + } + GapStatusDisplayItem gap=null; + if((flags&FLAG_NO_FOOTER)==0 && status.hasGapAfter!=null && !(fragment instanceof ThreadFragment)) + items.add(gap=new GapStatusDisplayItem(parentID, fragment, status)); + int i=1; + for(StatusDisplayItem item : items){ if(inset) item.inset=true; if(isForQuote){ @@ -367,15 +359,28 @@ public abstract class StatusDisplayItem{ } item.index=i++; } - } + if(items!=contentItems && !statusForContent.spoilerRevealed){ + for(StatusDisplayItem item : contentItems){ + if(inset) + item.inset=true; + if(isForQuote){ + item.status=statusForContent; + item.isForQuote=true; + } + item.index=i++; + } + } - List nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items; - WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null : - new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter); - return applyingFilter==null ? items : new ArrayList<>(gap!=null - ? List.of(warning, gap) - : Collections.singletonList(warning) - ); + List nonGapItems=gap!=null ? items.subList(0, items.size()-1) : items; + WarningFilteredStatusDisplayItem warning=applyingFilter==null ? null : + new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, nonGapItems, applyingFilter); + return applyingFilter==null ? items : new ArrayList<>(gap!=null + ? List.of(warning, gap) + : Collections.singletonList(warning) + ); + } catch(Exception e) { + return new ArrayList<>(Collections.singletonList(new ErrorStatusDisplayItem(parentID, fragment, e))); + } } public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List items){ @@ -411,6 +416,7 @@ public abstract class StatusDisplayItem{ SECTION_HEADER, HEADER_CHECKABLE, NOTIFICATION_HEADER, + ERROR_ITEM, FILTER_SPOILER, DUMMY }