Timeline refactor (#2175)
* Move Timeline files into their own package * Introduce TimelineViewModel, add coroutines * Simplify StatusViewData * Handle timeilne fetch errors * Rework filters, fix ViewThreadFragment * Fix NotificationsFragment * Simplify Notifications and Thread, handle pin * Redo loading in TimelineViewModel * Improve error handling in TimelineViewModel * Rewrite actions in TimelineViewModel * Apply feedback after timeline factoring review * Handle initial failure in timeline correctly
This commit is contained in:
parent
0a992480c2
commit
44a5b42cac
58 changed files with 3956 additions and 3618 deletions
|
|
@ -19,12 +19,5 @@ data class AttachmentViewData(
|
|||
AttachmentViewData(it, actionable.id, actionable.url!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun list(attachments: List<Attachment>): List<AttachmentViewData> {
|
||||
return attachments.map {
|
||||
AttachmentViewData(it, it.id, it.url)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -86,9 +86,7 @@ public abstract class NotificationViewData {
|
|||
return type == concrete.type &&
|
||||
Objects.equals(id, concrete.id) &&
|
||||
account.getId().equals(concrete.account.getId()) &&
|
||||
(statusViewData == concrete.statusViewData ||
|
||||
statusViewData != null &&
|
||||
statusViewData.deepEquals(concrete.statusViewData));
|
||||
(Objects.equals(statusViewData, concrete.statusViewData));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -96,6 +94,10 @@ public abstract class NotificationViewData {
|
|||
|
||||
return Objects.hash(type, id, account, statusViewData);
|
||||
}
|
||||
|
||||
public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) {
|
||||
return new Concrete(type, id, account, statusViewData);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Placeholder extends NotificationViewData {
|
||||
|
|
|
|||
|
|
@ -1,677 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.viewdata;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Created by charlag on 11/07/2017.
|
||||
* <p>
|
||||
* Class to represent data required to display either a notification or a placeholder.
|
||||
* It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}.
|
||||
*/
|
||||
|
||||
public abstract class StatusViewData {
|
||||
|
||||
private StatusViewData() { }
|
||||
|
||||
public abstract long getViewDataId();
|
||||
|
||||
public abstract boolean deepEquals(StatusViewData other);
|
||||
|
||||
public static final class Concrete extends StatusViewData {
|
||||
private static final char SOFT_HYPHEN = '\u00ad';
|
||||
private static final char ASCII_HYPHEN = '-';
|
||||
|
||||
private final String id;
|
||||
private final Spanned content;
|
||||
final boolean reblogged;
|
||||
final boolean favourited;
|
||||
final boolean bookmarked;
|
||||
private final boolean muted;
|
||||
@Nullable
|
||||
private final String spoilerText;
|
||||
private final Status.Visibility visibility;
|
||||
private final List<Attachment> attachments;
|
||||
@Nullable
|
||||
private final String rebloggedByUsername;
|
||||
@Nullable
|
||||
private final String rebloggedAvatar;
|
||||
private final boolean isSensitive;
|
||||
final boolean isExpanded;
|
||||
private final boolean isShowingContent;
|
||||
private final String userFullName;
|
||||
private final String nickname;
|
||||
private final String avatar;
|
||||
private final Date createdAt;
|
||||
private final int reblogsCount;
|
||||
private final int favouritesCount;
|
||||
@Nullable
|
||||
private final String inReplyToId;
|
||||
// I would rather have something else but it would be too much of a rewrite
|
||||
@Nullable
|
||||
private final Status.Mention[] mentions;
|
||||
private final String senderId;
|
||||
private final boolean rebloggingEnabled;
|
||||
private final Status.Application application;
|
||||
private final List<Emoji> statusEmojis;
|
||||
private final List<Emoji> accountEmojis;
|
||||
private final List<Emoji> rebloggedByAccountEmojis;
|
||||
@Nullable
|
||||
private final Card card;
|
||||
private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */
|
||||
final boolean isCollapsed; /** Whether the status is shown partially or fully */
|
||||
@Nullable
|
||||
private final PollViewData poll;
|
||||
private final boolean isBot;
|
||||
|
||||
public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, boolean muted,
|
||||
@Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments,
|
||||
@Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
|
||||
boolean isShowingContent, String userFullName, String nickname, String avatar,
|
||||
Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
|
||||
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
||||
Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, List<Emoji> rebloggedByAccountEmojis, @Nullable Card card,
|
||||
boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot) {
|
||||
|
||||
this.id = id;
|
||||
if (Build.VERSION.SDK_INT == 23) {
|
||||
// https://github.com/tuskyapp/Tusky/issues/563
|
||||
this.content = replaceCrashingCharacters(content);
|
||||
this.spoilerText = spoilerText == null ? null : replaceCrashingCharacters(spoilerText).toString();
|
||||
this.nickname = replaceCrashingCharacters(nickname).toString();
|
||||
} else {
|
||||
this.content = content;
|
||||
this.spoilerText = spoilerText;
|
||||
this.nickname = nickname;
|
||||
}
|
||||
this.reblogged = reblogged;
|
||||
this.favourited = favourited;
|
||||
this.bookmarked = bookmarked;
|
||||
this.muted = muted;
|
||||
this.visibility = visibility;
|
||||
this.attachments = attachments;
|
||||
this.rebloggedByUsername = rebloggedByUsername;
|
||||
this.rebloggedAvatar = rebloggedAvatar;
|
||||
this.isSensitive = sensitive;
|
||||
this.isExpanded = isExpanded;
|
||||
this.isShowingContent = isShowingContent;
|
||||
this.userFullName = userFullName;
|
||||
this.avatar = avatar;
|
||||
this.createdAt = createdAt;
|
||||
this.reblogsCount = reblogsCount;
|
||||
this.favouritesCount = favouritesCount;
|
||||
this.inReplyToId = inReplyToId;
|
||||
this.mentions = mentions;
|
||||
this.senderId = senderId;
|
||||
this.rebloggingEnabled = rebloggingEnabled;
|
||||
this.application = application;
|
||||
this.statusEmojis = statusEmojis;
|
||||
this.accountEmojis = accountEmojis;
|
||||
this.rebloggedByAccountEmojis = rebloggedByAccountEmojis;
|
||||
this.card = card;
|
||||
this.isCollapsible = isCollapsible;
|
||||
this.isCollapsed = isCollapsed;
|
||||
this.poll = poll;
|
||||
this.isBot = isBot;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Spanned getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public boolean isReblogged() {
|
||||
return reblogged;
|
||||
}
|
||||
|
||||
public boolean isFavourited() {
|
||||
return favourited;
|
||||
}
|
||||
|
||||
public boolean isBookmarked() {
|
||||
return bookmarked;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return muted;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSpoilerText() {
|
||||
return spoilerText;
|
||||
}
|
||||
|
||||
public Status.Visibility getVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getRebloggedByUsername() {
|
||||
return rebloggedByUsername;
|
||||
}
|
||||
|
||||
public boolean isSensitive() {
|
||||
return isSensitive;
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return isExpanded;
|
||||
}
|
||||
|
||||
public boolean isShowingContent() {
|
||||
return isShowingContent;
|
||||
}
|
||||
|
||||
public boolean isBot(){ return isBot; }
|
||||
|
||||
@Nullable
|
||||
public String getRebloggedAvatar() {
|
||||
return rebloggedAvatar;
|
||||
}
|
||||
|
||||
public String getUserFullName() {
|
||||
return userFullName;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public int getReblogsCount() {
|
||||
return reblogsCount;
|
||||
}
|
||||
|
||||
public int getFavouritesCount() {
|
||||
return favouritesCount;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getInReplyToId() {
|
||||
return inReplyToId;
|
||||
}
|
||||
|
||||
public String getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
|
||||
public Boolean getRebloggingEnabled() {
|
||||
return rebloggingEnabled;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Status.Mention[] getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public Status.Application getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public List<Emoji> getStatusEmojis() {
|
||||
return statusEmojis;
|
||||
}
|
||||
|
||||
public List<Emoji> getAccountEmojis() {
|
||||
return accountEmojis;
|
||||
}
|
||||
|
||||
public List<Emoji> getRebloggedByAccountEmojis() {
|
||||
return rebloggedByAccountEmojis;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Card getCard() {
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
||||
* all content regardless.
|
||||
*
|
||||
* @return Whether the post is collapsible or never collapsed.
|
||||
*/
|
||||
public boolean isCollapsible() {
|
||||
return isCollapsible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether the content of this post is currently limited in visibility to the first
|
||||
* 500 characters or not.
|
||||
*
|
||||
* @return Whether the post is collapsed or fully expanded.
|
||||
*/
|
||||
public boolean isCollapsed() {
|
||||
return isCollapsed;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PollViewData getPoll() {
|
||||
return poll;
|
||||
}
|
||||
|
||||
@Override public long getViewDataId() {
|
||||
// Chance of collision is super low and impact of mistake is low as well
|
||||
return id.hashCode();
|
||||
}
|
||||
|
||||
public boolean deepEquals(StatusViewData o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Concrete concrete = (Concrete) o;
|
||||
return reblogged == concrete.reblogged &&
|
||||
favourited == concrete.favourited &&
|
||||
bookmarked == concrete.bookmarked &&
|
||||
isSensitive == concrete.isSensitive &&
|
||||
isExpanded == concrete.isExpanded &&
|
||||
isShowingContent == concrete.isShowingContent &&
|
||||
isBot == concrete.isBot &&
|
||||
reblogsCount == concrete.reblogsCount &&
|
||||
favouritesCount == concrete.favouritesCount &&
|
||||
rebloggingEnabled == concrete.rebloggingEnabled &&
|
||||
Objects.equals(id, concrete.id) &&
|
||||
Objects.equals(content, concrete.content) &&
|
||||
Objects.equals(spoilerText, concrete.spoilerText) &&
|
||||
visibility == concrete.visibility &&
|
||||
Objects.equals(attachments, concrete.attachments) &&
|
||||
Objects.equals(rebloggedByUsername, concrete.rebloggedByUsername) &&
|
||||
Objects.equals(rebloggedAvatar, concrete.rebloggedAvatar) &&
|
||||
Objects.equals(userFullName, concrete.userFullName) &&
|
||||
Objects.equals(nickname, concrete.nickname) &&
|
||||
Objects.equals(avatar, concrete.avatar) &&
|
||||
Objects.equals(createdAt, concrete.createdAt) &&
|
||||
Objects.equals(inReplyToId, concrete.inReplyToId) &&
|
||||
Arrays.equals(mentions, concrete.mentions) &&
|
||||
Objects.equals(senderId, concrete.senderId) &&
|
||||
Objects.equals(application, concrete.application) &&
|
||||
Objects.equals(statusEmojis, concrete.statusEmojis) &&
|
||||
Objects.equals(accountEmojis, concrete.accountEmojis) &&
|
||||
Objects.equals(rebloggedByAccountEmojis, concrete.rebloggedByAccountEmojis) &&
|
||||
Objects.equals(card, concrete.card) &&
|
||||
Objects.equals(poll, concrete.poll)
|
||||
&& isCollapsed == concrete.isCollapsed;
|
||||
}
|
||||
|
||||
static Spanned replaceCrashingCharacters(Spanned content) {
|
||||
return (Spanned) replaceCrashingCharacters((CharSequence) content);
|
||||
}
|
||||
|
||||
static CharSequence replaceCrashingCharacters(CharSequence content) {
|
||||
boolean replacing = false;
|
||||
SpannableStringBuilder builder = null;
|
||||
int length = content.length();
|
||||
|
||||
for (int index = 0; index < length; ++index) {
|
||||
char character = content.charAt(index);
|
||||
|
||||
// If there are more than one or two, switch to a map
|
||||
if (character == SOFT_HYPHEN) {
|
||||
if (!replacing) {
|
||||
replacing = true;
|
||||
builder = new SpannableStringBuilder(content, 0, index);
|
||||
}
|
||||
builder.append(ASCII_HYPHEN);
|
||||
} else if (replacing) {
|
||||
builder.append(character);
|
||||
}
|
||||
}
|
||||
|
||||
return replacing ? builder : content;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Placeholder extends StatusViewData {
|
||||
private final boolean isLoading;
|
||||
private final String id;
|
||||
|
||||
public Placeholder(String id, boolean isLoading) {
|
||||
this.id = id;
|
||||
this.isLoading = isLoading;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override public long getViewDataId() {
|
||||
return id.hashCode();
|
||||
}
|
||||
|
||||
@Override public boolean deepEquals(StatusViewData other) {
|
||||
if (!(other instanceof Placeholder)) return false;
|
||||
Placeholder that = (Placeholder) other;
|
||||
return isLoading == that.isLoading && id.equals(that.id);
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Placeholder that = (Placeholder) o;
|
||||
|
||||
return deepEquals(that);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (isLoading ? 1 : 0);
|
||||
result = 31 * result + id.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String id;
|
||||
private Spanned content;
|
||||
private boolean reblogged;
|
||||
private boolean favourited;
|
||||
private boolean bookmarked;
|
||||
private boolean muted;
|
||||
private String spoilerText;
|
||||
private Status.Visibility visibility;
|
||||
private List<Attachment> attachments;
|
||||
private String rebloggedByUsername;
|
||||
private String rebloggedAvatar;
|
||||
private boolean isSensitive;
|
||||
private boolean isExpanded;
|
||||
private boolean isShowingContent;
|
||||
private String userFullName;
|
||||
private String nickname;
|
||||
private String avatar;
|
||||
private Date createdAt;
|
||||
private int reblogsCount;
|
||||
private int favouritesCount;
|
||||
private String inReplyToId;
|
||||
private Status.Mention[] mentions;
|
||||
private String senderId;
|
||||
private boolean rebloggingEnabled;
|
||||
private Status.Application application;
|
||||
private List<Emoji> statusEmojis;
|
||||
private List<Emoji> accountEmojis;
|
||||
private List<Emoji> rebloggedByAccountEmojis;
|
||||
private Card card;
|
||||
private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */
|
||||
private boolean isCollapsed; /** Whether the status is shown partially or fully */
|
||||
private PollViewData poll;
|
||||
private boolean isBot;
|
||||
|
||||
public Builder() {
|
||||
}
|
||||
|
||||
public Builder(final StatusViewData.Concrete viewData) {
|
||||
id = viewData.id;
|
||||
content = viewData.content;
|
||||
reblogged = viewData.reblogged;
|
||||
favourited = viewData.favourited;
|
||||
bookmarked = viewData.bookmarked;
|
||||
muted = viewData.muted;
|
||||
spoilerText = viewData.spoilerText;
|
||||
visibility = viewData.visibility;
|
||||
attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments);
|
||||
rebloggedByUsername = viewData.rebloggedByUsername;
|
||||
rebloggedAvatar = viewData.rebloggedAvatar;
|
||||
isSensitive = viewData.isSensitive;
|
||||
isExpanded = viewData.isExpanded;
|
||||
isShowingContent = viewData.isShowingContent;
|
||||
userFullName = viewData.userFullName;
|
||||
nickname = viewData.nickname;
|
||||
avatar = viewData.avatar;
|
||||
createdAt = new Date(viewData.createdAt.getTime());
|
||||
reblogsCount = viewData.reblogsCount;
|
||||
favouritesCount = viewData.favouritesCount;
|
||||
inReplyToId = viewData.inReplyToId;
|
||||
mentions = viewData.mentions == null ? null : viewData.mentions.clone();
|
||||
senderId = viewData.senderId;
|
||||
rebloggingEnabled = viewData.rebloggingEnabled;
|
||||
application = viewData.application;
|
||||
statusEmojis = viewData.getStatusEmojis();
|
||||
accountEmojis = viewData.getAccountEmojis();
|
||||
rebloggedByAccountEmojis = viewData.getRebloggedByAccountEmojis();
|
||||
card = viewData.getCard();
|
||||
isCollapsible = viewData.isCollapsible();
|
||||
isCollapsed = viewData.isCollapsed();
|
||||
poll = viewData.poll;
|
||||
isBot = viewData.isBot();
|
||||
}
|
||||
|
||||
public Builder setId(String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setContent(Spanned content) {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setReblogged(boolean reblogged) {
|
||||
this.reblogged = reblogged;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFavourited(boolean favourited) {
|
||||
this.favourited = favourited;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBookmarked(boolean bookmarked) {
|
||||
this.bookmarked = bookmarked;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMuted(boolean muted) {
|
||||
this.muted = muted;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSpoilerText(String spoilerText) {
|
||||
this.spoilerText = spoilerText;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVisibility(Status.Visibility visibility) {
|
||||
this.visibility = visibility;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAttachments(List<Attachment> attachments) {
|
||||
this.attachments = attachments;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRebloggedByUsername(String rebloggedByUsername) {
|
||||
this.rebloggedByUsername = rebloggedByUsername;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRebloggedAvatar(String rebloggedAvatar) {
|
||||
this.rebloggedAvatar = rebloggedAvatar;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSensitive(boolean sensitive) {
|
||||
this.isSensitive = sensitive;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIsExpanded(boolean isExpanded) {
|
||||
this.isExpanded = isExpanded;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIsShowingSensitiveContent(boolean isShowingSensitiveContent) {
|
||||
this.isShowingContent = isShowingSensitiveContent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIsBot(boolean isBot) {
|
||||
this.isBot = isBot;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUserFullName(String userFullName) {
|
||||
this.userFullName = userFullName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAvatar(String avatar) {
|
||||
this.avatar = avatar;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setReblogsCount(int reblogsCount) {
|
||||
this.reblogsCount = reblogsCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFavouritesCount(int favouritesCount) {
|
||||
this.favouritesCount = favouritesCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setInReplyToId(String inReplyToId) {
|
||||
this.inReplyToId = inReplyToId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMentions(Status.Mention[] mentions) {
|
||||
this.mentions = mentions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setSenderId(String senderId) {
|
||||
this.senderId = senderId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRebloggingEnabled(boolean rebloggingEnabled) {
|
||||
this.rebloggingEnabled = rebloggingEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setApplication(Status.Application application) {
|
||||
this.application = application;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setStatusEmojis(List<Emoji> emojis) {
|
||||
this.statusEmojis = emojis;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAccountEmojis(List<Emoji> emojis) {
|
||||
this.accountEmojis = emojis;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRebloggedByEmojis(List<Emoji> emojis) {
|
||||
this.rebloggedByAccountEmojis = emojis;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCard(Card card) {
|
||||
this.card = card;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to support collapsing
|
||||
* its content limiting the visible length when collapsed at 500 characters,
|
||||
*
|
||||
* @param collapsible Whether the status should support being collapsed or not.
|
||||
* @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance.
|
||||
*/
|
||||
public Builder setCollapsible(boolean collapsible) {
|
||||
isCollapsible = collapsible;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to start in a collapsed
|
||||
* state, hiding partially the content of the post if it exceeds a certain amount of characters.
|
||||
*
|
||||
* @param collapsed Whether to show the full content of the status or not.
|
||||
* @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance.
|
||||
*/
|
||||
public Builder setCollapsed(boolean collapsed) {
|
||||
isCollapsed = collapsed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPoll(Poll poll) {
|
||||
this.poll = PollViewDataKt.toViewData(poll);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatusViewData.Concrete createStatusViewData() {
|
||||
if (this.statusEmojis == null) statusEmojis = Collections.emptyList();
|
||||
if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
|
||||
if (this.createdAt == null) createdAt = new Date();
|
||||
|
||||
return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, muted, spoilerText,
|
||||
visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
||||
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
|
||||
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
|
||||
statusEmojis, accountEmojis, rebloggedByAccountEmojis, card, isCollapsible, isCollapsed, poll, isBot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.viewdata
|
||||
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
||||
/**
|
||||
* Created by charlag on 11/07/2017.
|
||||
*
|
||||
*
|
||||
* Class to represent data required to display either a notification or a placeholder.
|
||||
* It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder].
|
||||
*/
|
||||
sealed class StatusViewData private constructor() {
|
||||
abstract val viewDataId: Long
|
||||
|
||||
data class Concrete(
|
||||
val status: Status,
|
||||
val isExpanded: Boolean,
|
||||
val isShowingContent: Boolean,
|
||||
/**
|
||||
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
||||
* all content regardless.
|
||||
*
|
||||
* @return Whether the post is collapsible or never collapsed.
|
||||
*/
|
||||
val isCollapsible: Boolean,
|
||||
/**
|
||||
* Specifies whether the content of this post is currently limited in visibility to the first
|
||||
* 500 characters or not.
|
||||
*
|
||||
* @return Whether the post is collapsed or fully expanded.
|
||||
*/
|
||||
/** Whether the status meets the requirement to be collapse */
|
||||
val isCollapsed: Boolean,
|
||||
) : StatusViewData() {
|
||||
override val viewDataId: Long
|
||||
get() = status.id.hashCode().toLong()
|
||||
|
||||
val content: Spanned
|
||||
val spoilerText: String
|
||||
val username: String
|
||||
|
||||
val actionable: Status
|
||||
get() = status.actionableStatus
|
||||
|
||||
val rebloggedAvatar: String?
|
||||
get() = status.reblog?.account?.avatar
|
||||
|
||||
val rebloggingStatus: Status?
|
||||
get() = if (status.reblog != null) status else null
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT == 23) {
|
||||
// https://github.com/tuskyapp/Tusky/issues/563
|
||||
this.content = replaceCrashingCharacters(status.actionableStatus.content)
|
||||
this.spoilerText =
|
||||
replaceCrashingCharacters(status.actionableStatus.spoilerText).toString()
|
||||
this.username =
|
||||
replaceCrashingCharacters(status.actionableStatus.account.username).toString()
|
||||
} else {
|
||||
this.content = status.actionableStatus.content
|
||||
this.spoilerText = status.actionableStatus.spoilerText
|
||||
this.username = status.actionableStatus.account.username
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SOFT_HYPHEN = '\u00ad'
|
||||
private const val ASCII_HYPHEN = '-'
|
||||
fun replaceCrashingCharacters(content: Spanned): Spanned {
|
||||
return replaceCrashingCharacters(content as CharSequence) as Spanned
|
||||
}
|
||||
|
||||
fun replaceCrashingCharacters(content: CharSequence?): CharSequence? {
|
||||
var replacing = false
|
||||
var builder: SpannableStringBuilder? = null
|
||||
val length = content!!.length
|
||||
for (index in 0 until length) {
|
||||
val character = content[index]
|
||||
|
||||
// If there are more than one or two, switch to a map
|
||||
if (character == SOFT_HYPHEN) {
|
||||
if (!replacing) {
|
||||
replacing = true
|
||||
builder = SpannableStringBuilder(content, 0, index)
|
||||
}
|
||||
builder!!.append(ASCII_HYPHEN)
|
||||
} else if (replacing) {
|
||||
builder!!.append(character)
|
||||
}
|
||||
}
|
||||
return if (replacing) builder else content
|
||||
}
|
||||
}
|
||||
|
||||
val id: String
|
||||
get() = status.id
|
||||
|
||||
/** Helper for Java */
|
||||
fun copyWithStatus(status: Status): Concrete {
|
||||
return copy(status = status)
|
||||
}
|
||||
|
||||
/** Helper for Java */
|
||||
fun copyWithExpanded(isExpanded: Boolean): Concrete {
|
||||
return copy(isExpanded = isExpanded)
|
||||
}
|
||||
|
||||
/** Helper for Java */
|
||||
fun copyWithShowingContent(isShowingContent: Boolean): Concrete {
|
||||
return copy(isShowingContent = isShowingContent)
|
||||
}
|
||||
|
||||
/** Helper for Java */
|
||||
fun copyWIthCollapsed(isCollapsed: Boolean): Concrete {
|
||||
return copy(isCollapsed = isCollapsed)
|
||||
}
|
||||
}
|
||||
|
||||
data class Placeholder(val id: String, val isLoading: Boolean) : StatusViewData() {
|
||||
override val viewDataId: Long
|
||||
get() = id.hashCode().toLong()
|
||||
}
|
||||
|
||||
fun asStatusOrNull() = this as? Concrete
|
||||
|
||||
fun asPlaceholderOrNull() = this as? Placeholder
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue