Fix loading toots with conversation_id as String

It happens at Gleasonator but it could happen to other instances. The
ones which returns this value as an Integer should work perfectly.

Fixes: https://todo.sr.ht/~captainepoch/husky/48
This commit is contained in:
Adolfo Santiago 2022-07-07 16:48:36 +02:00
parent 06b01e5a37
commit 0bd5182b9e
No known key found for this signature in database
GPG key ID: 244D6F9A317B4A65
6 changed files with 203 additions and 150 deletions

View file

@ -1,17 +1,22 @@
/* Copyright 2017 Andrew Dawson /*
* Husky -- A Pleroma client for Android
* *
* This file is a part of Tusky. * Copyright (C) 2022 The Husky Developers
* Copyright (C) 2017 Andrew Dawson
* *
* This program is free software; you can redistribute it and/or modify it under the terms of the * This program is free software: you can redistribute it and/or modify
* GNU General Public License as published by the Free Software Foundation; either version 3 of the * it under the terms of the GNU General Public License as published by
* License, or (at your option) any later version. * 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 * This program is distributed in the hope that it will be useful,
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * but WITHOUT ANY WARRANTY; without even the implied warranty of
* Public License for more details. * 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, * You should have received a copy of the GNU General Public License
* see <http://www.gnu.org/licenses>. */ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
@ -19,35 +24,35 @@ import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.style.URLSpan import android.text.style.URLSpan
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import java.util.* import java.util.Date
data class Status( data class Status(
var id: String, var id: String,
var url: String?, // not present if it's reblog var url: String?, // not present if it's reblog
val account: Account, val account: Account,
@SerializedName("in_reply_to_id") var inReplyToId: String?, @SerializedName("in_reply_to_id") var inReplyToId: String?,
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
val reblog: Status?, val reblog: Status?,
val content: Spanned, val content: Spanned,
@SerializedName("created_at") val createdAt: Date, @SerializedName("created_at") val createdAt: Date,
val emojis: List<Emoji>, val emojis: List<Emoji>,
@SerializedName("reblogs_count") val reblogsCount: Int, @SerializedName("reblogs_count") val reblogsCount: Int,
@SerializedName("favourites_count") val favouritesCount: Int, @SerializedName("favourites_count") val favouritesCount: Int,
var reblogged: Boolean, var reblogged: Boolean,
var favourited: Boolean, var favourited: Boolean,
var bookmarked: Boolean, var bookmarked: Boolean,
var sensitive: Boolean, var sensitive: Boolean,
@SerializedName("spoiler_text") val spoilerText: String, @SerializedName("spoiler_text") val spoilerText: String,
val visibility: Visibility, val visibility: Visibility,
@SerializedName("media_attachments") var attachments: ArrayList<Attachment>, @SerializedName("media_attachments") var attachments: ArrayList<Attachment>,
val mentions: Array<Mention>, val mentions: Array<Mention>,
val application: Application?, val application: Application?,
var pinned: Boolean?, var pinned: Boolean?,
val poll: Poll?, val poll: Poll?,
val card: Card?, val card: Card?,
var content_type: String? = null, var content_type: String? = null,
val pleroma: PleromaStatus? = null, val pleroma: PleromaStatus? = null,
var muted: Boolean = false /* set when either thread or user is muted */ var muted: Boolean = false /* set when either thread or user is muted */
) { ) {
val actionableId: String val actionableId: String
@ -58,17 +63,21 @@ data class Status(
enum class Visibility(val num: Int) { enum class Visibility(val num: Int) {
UNKNOWN(0), UNKNOWN(0),
@SerializedName("public") @SerializedName("public")
PUBLIC(1), PUBLIC(1),
@SerializedName("unlisted") @SerializedName("unlisted")
UNLISTED(2), UNLISTED(2),
@SerializedName("private") @SerializedName("private")
PRIVATE(3), PRIVATE(3),
@SerializedName("direct") @SerializedName("direct")
DIRECT(4); DIRECT(4);
fun serverString(): String { fun serverString(): String {
return when (this) { return when(this) {
PUBLIC -> "public" PUBLIC -> "public"
UNLISTED -> "unlisted" UNLISTED -> "unlisted"
PRIVATE -> "private" PRIVATE -> "private"
@ -81,7 +90,7 @@ data class Status(
@JvmStatic @JvmStatic
fun byNum(num: Int): Visibility { fun byNum(num: Int): Visibility {
return when (num) { return when(num) {
4 -> DIRECT 4 -> DIRECT
3 -> PRIVATE 3 -> PRIVATE
2 -> UNLISTED 2 -> UNLISTED
@ -93,7 +102,7 @@ data class Status(
@JvmStatic @JvmStatic
fun byString(s: String): Visibility { fun byString(s: String): Visibility {
return when (s) { return when(s) {
"public" -> PUBLIC "public" -> PUBLIC
"unlisted" -> UNLISTED "unlisted" -> UNLISTED
"private" -> PRIVATE "private" -> PRIVATE
@ -115,14 +124,14 @@ data class Status(
fun toDeletedStatus(): DeletedStatus { fun toDeletedStatus(): DeletedStatus {
return DeletedStatus( return DeletedStatus(
text = getEditableText(), text = getEditableText(),
inReplyToId = inReplyToId, inReplyToId = inReplyToId,
spoilerText = spoilerText, spoilerText = spoilerText,
visibility = visibility, visibility = visibility,
sensitive = sensitive, sensitive = sensitive,
attachments = attachments, attachments = attachments,
poll = poll, poll = poll,
createdAt = createdAt createdAt = createdAt
) )
} }
@ -143,8 +152,8 @@ data class Status(
pleroma.threadMuted = mute pleroma.threadMuted = mute
} }
fun getConversationId(): Int { fun getConversationId(): String {
return pleroma?.conversationId ?: -1 return pleroma?.conversationId ?: ""
} }
fun getEmojiReactions(): List<EmojiReaction>? { fun getEmojiReactions(): List<EmojiReaction>? {
@ -161,10 +170,10 @@ data class Status(
private fun getEditableText(): String { private fun getEditableText(): String {
val builder = SpannableStringBuilder(content) val builder = SpannableStringBuilder(content)
for (span in content.getSpans(0, content.length, URLSpan::class.java)) { for(span in content.getSpans(0, content.length, URLSpan::class.java)) {
val url = span.url val url = span.url
for ((_, url1, username) in mentions) { for((_, url1, username) in mentions) {
if (url == url1) { if(url == url1) {
val start = builder.getSpanStart(span) val start = builder.getSpanStart(span)
val end = builder.getSpanEnd(span) val end = builder.getSpanEnd(span)
builder.replace(start, end, "@$username") builder.replace(start, end, "@$username")
@ -176,8 +185,8 @@ data class Status(
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if(this === other) return true
if (other == null || javaClass != other.javaClass) return false if(other == null || javaClass != other.javaClass) return false
val status = other as Status? val status = other as Status?
return id == status?.id return id == status?.id
@ -189,20 +198,20 @@ data class Status(
data class PleromaStatus( data class PleromaStatus(
@SerializedName("thread_muted") var threadMuted: Boolean?, @SerializedName("thread_muted") var threadMuted: Boolean?,
@SerializedName("conversation_id") val conversationId: Int?, @SerializedName("conversation_id") val conversationId: String?,
@SerializedName("emoji_reactions") val emojiReactions: List<EmojiReaction>?, @SerializedName("emoji_reactions") val emojiReactions: List<EmojiReaction>?,
@SerializedName("in_reply_to_account_acct") val inReplyToAccountAcct: String?, @SerializedName("in_reply_to_account_acct") val inReplyToAccountAcct: String?,
@SerializedName("parent_visible") val parentVisible: Boolean? @SerializedName("parent_visible") val parentVisible: Boolean?
) )
data class Mention ( data class Mention(
val id: String, val id: String,
val url: String?, // can be null due to bug in some Pleroma versions val url: String?, // can be null due to bug in some Pleroma versions
@SerializedName("acct") val username: String, @SerializedName("acct") val username: String,
@SerializedName("username") val localUsername: String @SerializedName("username") val localUsername: String
) )
data class Application ( data class Application(
val name: String, val name: String,
val website: String? val website: String?
) )

View file

@ -350,9 +350,9 @@ public class NotificationsFragment extends SFragment implements
if (posAndNotification == null) if (posAndNotification == null)
return; return;
int conversationId = posAndNotification.second.getStatus().getConversationId(); String conversationId = posAndNotification.second.getStatus().getConversationId();
if(conversationId == -1) { // invalid conversation ID if(conversationId.isEmpty()) { // invalid conversation ID
if(withMuted) { if(withMuted) {
setMutedStatusForStatus(posAndNotification.first, posAndNotification.second.getStatus(), event.getMute(), event.getMute()); setMutedStatusForStatus(posAndNotification.first, posAndNotification.second.getStatus(), event.getMute(), event.getMute());
} else { } else {
@ -1016,7 +1016,7 @@ public class NotificationsFragment extends SFragment implements
updateAdapter(); updateAdapter();
} }
private void removeAllByConversationId(int conversationId) { private void removeAllByConversationId(String conversationId) {
// using iterator to safely remove items while iterating // using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator(); Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
@ -1024,7 +1024,7 @@ public class NotificationsFragment extends SFragment implements
Notification notification = placeholderOrNotification.asRightOrNull(); Notification notification = placeholderOrNotification.asRightOrNull();
if (notification != null && notification.getStatus() != null if (notification != null && notification.getStatus() != null
&& notification.getType() == Notification.Type.MENTION && && notification.getType() == Notification.Type.MENTION &&
notification.getStatus().getConversationId() == conversationId) { notification.getStatus().getConversationId().equalsIgnoreCase(conversationId)) {
iterator.remove(); iterator.remove();
} }
} }

View file

@ -1,17 +1,23 @@
/* Copyright 2017 Andrew Dawson /*
* Husky -- A Pleroma client for Android
* *
* This file is a part of Tusky. * Copyright (C) 2022 The Husky Developers
* Copyright (C) 2017 Andrew Dawson
* *
* This program is free software; you can redistribute it and/or modify it under the terms of the * This program is free software: you can redistribute it and/or modify
* GNU General Public License as published by the Free Software Foundation; either version 3 of the * it under the terms of the GNU General Public License as published by
* License, or (at your option) any later version. * 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 * This program is distributed in the hope that it will be useful,
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * but WITHOUT ANY WARRANTY; without even the implied warranty of
* Public License for more details. * 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, * You should have received a copy of the GNU General Public License
* see <http://www.gnu.org/licenses>. */ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.keylesspalace.tusky.fragment; package com.keylesspalace.tusky.fragment;
@ -22,7 +28,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -108,13 +113,13 @@ import kotlin.jvm.functions.Function1;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
import timber.log.Timber;
public class TimelineFragment extends SFragment implements public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
StatusActionListener, StatusActionListener,
Injectable, ReselectableFragment, RefreshableFragment { Injectable, ReselectableFragment, RefreshableFragment {
private static final String TAG = "TimelineF"; // logging tag
private static final String KIND_ARG = "kind"; private static final String KIND_ARG = "kind";
private static final String ID_ARG = "id"; private static final String ID_ARG = "id";
private static final String HASHTAGS_ARG = "hastags"; private static final String HASHTAGS_ARG = "hastags";
@ -302,7 +307,7 @@ public class TimelineFragment extends SFragment implements
// Request timeline from disk to make it quick, then replace it with timeline from // Request timeline from disk to make it quick, then replace it with timeline from
// the server to update it // the server to update it
timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE,
TimelineRequestMode.DISK) TimelineRequestMode.DISK)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(statuses -> { .subscribe(statuses -> {
@ -334,7 +339,7 @@ public class TimelineFragment extends SFragment implements
String topId = CollectionsKt.first(this.statuses, Either::isRight).asRight().getId(); String topId = CollectionsKt.first(this.statuses, Either::isRight).asRight().getId();
this.timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE, this.timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE,
TimelineRequestMode.NETWORK) TimelineRequestMode.NETWORK)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe( .subscribe(
@ -475,7 +480,7 @@ public class TimelineFragment extends SFragment implements
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
* guaranteed to be set until then. */ * guaranteed to be set until then. */
@ -623,7 +628,7 @@ public class TimelineFragment extends SFragment implements
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe( .subscribe(
(newStatus) -> setRebloggedForStatus(position, status, reblog), (newStatus) -> setRebloggedForStatus(position, status, reblog),
(err) -> Log.d(TAG, "Failed to reblog status " + status.getId(), err) (err) -> Timber.e("Failed to reblog status " + status.getId() + ", Error[" + err + "]")
); );
} }
@ -655,7 +660,7 @@ public class TimelineFragment extends SFragment implements
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe( .subscribe(
(newStatus) -> setFavouriteForStatus(position, newStatus, favourite), (newStatus) -> setFavouriteForStatus(position, newStatus, favourite),
(err) -> Log.d(TAG, "Failed to favourite status " + status.getId(), err) (err) -> Timber.e("Failed to favourite status " + status.getId() + ", Error [" + err + "]")
); );
} }
@ -687,7 +692,7 @@ public class TimelineFragment extends SFragment implements
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe( .subscribe(
(newStatus) -> setBookmarkForStatus(position, newStatus, bookmark), (newStatus) -> setBookmarkForStatus(position, newStatus, bookmark),
(err) -> Log.d(TAG, "Failed to favourite status " + status.getId(), err) (err) -> Timber.e(err, "Failed to favourite status " + status.getId())
); );
} }
@ -743,8 +748,8 @@ public class TimelineFragment extends SFragment implements
.as(autoDisposable(from(this))) .as(autoDisposable(from(this)))
.subscribe( .subscribe(
(newPoll) -> setVoteForPoll(position, status, newPoll), (newPoll) -> setVoteForPoll(position, status, newPoll),
(t) -> Log.d(TAG, (t) -> Timber.e(t,
"Failed to vote in poll: " + status.getId(), t) "Failed to vote in poll: " + status.getId())
); );
} }
@ -815,7 +820,7 @@ public class TimelineFragment extends SFragment implements
? statuses.get(position + 1).asRight().getId() ? statuses.get(position + 1).asRight().getId()
: null; : null;
if(fromStatus == null || toStatus == null) { if(fromStatus == null || toStatus == null) {
Log.e(TAG, "Failed to load more at " + position + ", wrong placeholder position"); Timber.e("Failed to load more at " + position + ", wrong placeholder position");
return; return;
} }
sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), maxMinusOne, sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), maxMinusOne,
@ -826,14 +831,14 @@ public class TimelineFragment extends SFragment implements
statuses.setPairedItem(position, newViewData); statuses.setPairedItem(position, newViewData);
updateAdapter(); updateAdapter();
} else { } else {
Log.e(TAG, "error loading more"); Timber.e("error loading more");
} }
} }
@Override @Override
public void onContentCollapsedChange(boolean isCollapsed, int position) { public void onContentCollapsedChange(boolean isCollapsed, int position) {
if(position < 0 || position >= statuses.size()) { if(position < 0 || position >= statuses.size()) {
Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size() - 1)); Timber.e(String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size() - 1));
return; return;
} }
@ -841,7 +846,7 @@ public class TimelineFragment extends SFragment implements
if(!(status instanceof StatusViewData.Concrete)) { if(!(status instanceof StatusViewData.Concrete)) {
// Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't
// check for null values when adding values to it although this doesn't seem to be an issue. // check for null values when adding values to it although this doesn't seem to be an issue.
Log.e(TAG, String.format( Timber.e(String.format(
"Expected StatusViewData.Concrete, got %s instead at position: %d of %d", "Expected StatusViewData.Concrete, got %s instead at position: %d of %d",
status == null ? "<null>" : status.getClass().getSimpleName(), status == null ? "<null>" : status.getClass().getSimpleName(),
position, position,
@ -960,13 +965,13 @@ public class TimelineFragment extends SFragment implements
updateAdapter(); updateAdapter();
} }
private void removeAllByConversationId(int conversationId) { private void removeAllByConversationId(String conversationId) {
// using iterator to safely remove items while iterating // using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator(); Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
while(iterator.hasNext()) { while(iterator.hasNext()) {
Status status = iterator.next().asRightOrNull(); Status status = iterator.next().asRightOrNull();
if(status != null && if(status != null &&
(status.getConversationId() == conversationId) || status.getActionableStatus().getConversationId() == conversationId) { (status.getConversationId().equalsIgnoreCase(conversationId)) || status.getActionableStatus().getConversationId().equalsIgnoreCase(conversationId)) {
iterator.remove(); iterator.remove();
} }
} }
@ -1249,7 +1254,7 @@ public class TimelineFragment extends SFragment implements
} }
} }
Log.e(TAG, "Fetch Failure: " + exception.getMessage()); Timber.e("Fetch Failure: %s", exception.getMessage());
updateBottomLoadingState(fetchEnd); updateBottomLoadingState(fetchEnd);
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
} }
@ -1453,9 +1458,9 @@ public class TimelineFragment extends SFragment implements
return; return;
Status eventStatus = statuses.get(pos).asRight(); Status eventStatus = statuses.get(pos).asRight();
int conversationId = eventStatus.getConversationId(); String conversationId = eventStatus.getConversationId();
if(conversationId == -1) { // invalid conversation ID if(conversationId.isEmpty()) { // invalid conversation ID
if(isFilteringMuted()) { if(isFilteringMuted()) {
statuses.remove(pos); statuses.remove(pos);
} else { } else {
@ -1650,13 +1655,12 @@ public class TimelineFragment extends SFragment implements
.as(autoDisposable(from(this))) .as(autoDisposable(from(this)))
.subscribe( .subscribe(
(newStatus) -> setEmojiReactionForStatus(position, newStatus), (newStatus) -> setEmojiReactionForStatus(position, newStatus),
(t) -> Log.d(TAG, (t) -> Timber.e(t,
"Failed to react with " + emoji + " on status: " + statusId, t) "Failed to react with " + emoji + " on status: " + statusId)
); );
} }
@Override @Override
public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId) { public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId) {
super.emojiReactMenu(statusId, emoji, view, this); super.emojiReactMenu(statusId, emoji, view, this);

View file

@ -1,3 +1,23 @@
/*
* Husky -- A Pleroma client for Android
*
* Copyright (C) 2022 The Husky Developers
* Copyright (C) 2018 Tusky Contributors
*
* 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.keylesspalace.tusky.repository package com.keylesspalace.tusky.repository
import android.text.SpannedString import android.text.SpannedString

View file

@ -53,6 +53,7 @@ object OkHttpUtils {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.addInterceptor(getUserAgentInterceptor()) .addInterceptor(getUserAgentInterceptor())
//.addInterceptor(getDebugInformation())
.addInterceptor(BrotliInterceptor) .addInterceptor(BrotliInterceptor)
.readTimeout(60, SECONDS) .readTimeout(60, SECONDS)
.writeTimeout(60, SECONDS) .writeTimeout(60, SECONDS)

View file

@ -1,33 +1,35 @@
/* Copyright 2017 Andrew Dawson /*
* Husky -- A Pleroma client for Android
* *
* This file is a part of Tusky. * Copyright (C) 2022 The Husky Developers
* Copyright (C) 2017 Andrew Dawson
* *
* This program is free software; you can redistribute it and/or modify it under the terms of the * This program is free software: you can redistribute it and/or modify
* GNU General Public License as published by the Free Software Foundation; either version 3 of the * it under the terms of the GNU General Public License as published by
* License, or (at your option) any later version. * 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 * This program is distributed in the hope that it will be useful,
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * but WITHOUT ANY WARRANTY; without even the implied warranty of
* Public License for more details. * 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, * You should have received a copy of the GNU General Public License
* see <http://www.gnu.org/licenses>. */ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.keylesspalace.tusky.viewdata; package com.keylesspalace.tusky.viewdata;
import android.os.Build; import android.os.Build;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.EmojiReaction; import com.keylesspalace.tusky.entity.EmojiReaction;
import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -36,15 +38,14 @@ import java.util.List;
import java.util.Objects; 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. * 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}. * It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}.
*/ */
public abstract class StatusViewData { public abstract class StatusViewData {
private StatusViewData() { } private StatusViewData() {
}
public abstract long getViewDataId(); public abstract long getViewDataId();
@ -91,15 +92,21 @@ public abstract class StatusViewData {
private final List<Emoji> rebloggedByAccountEmojis; private final List<Emoji> rebloggedByAccountEmojis;
@Nullable @Nullable
private final Card card; private final Card card;
private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */ private final boolean isCollapsible;
final boolean isCollapsed; /** Whether the status is shown partially or fully */ /**
* Whether the status meets the requirement to be collapse
*/
final boolean isCollapsed;
/**
* Whether the status is shown partially or fully
*/
@Nullable @Nullable
private final PollViewData poll; private final PollViewData poll;
private final boolean isBot; private final boolean isBot;
private final boolean isMuted; /* user toggle */ private final boolean isMuted; /* user toggle */
private final boolean isThreadMuted; /* thread_muted state got from backend */ private final boolean isThreadMuted; /* thread_muted state got from backend */
private final boolean isUserMuted; /* muted state got from backend */ private final boolean isUserMuted; /* muted state got from backend */
private final int conversationId; private final String conversationId;
@Nullable @Nullable
private final List<EmojiReaction> emojiReactions; private final List<EmojiReaction> emojiReactions;
private final boolean parentVisible; private final boolean parentVisible;
@ -112,10 +119,10 @@ public abstract class StatusViewData {
@Nullable String inReplyToAccountAcct, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, @Nullable String inReplyToAccountAcct, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, List<Emoji> rebloggedByAccountEmojis, @Nullable Card card, Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, List<Emoji> rebloggedByAccountEmojis, @Nullable Card card,
boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, boolean isMuted, boolean isThreadMuted, boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, boolean isMuted, boolean isThreadMuted,
boolean isUserMuted, int conversationId, @Nullable List<EmojiReaction> emojiReactions, boolean parentVisible) { boolean isUserMuted, String conversationId, @Nullable List<EmojiReaction> emojiReactions, boolean parentVisible) {
this.id = id; this.id = id;
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { if(Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
// https://github.com/tuskyapp/Tusky/issues/563 // https://github.com/tuskyapp/Tusky/issues/563
this.content = replaceCrashingCharacters(content); this.content = replaceCrashingCharacters(content);
this.spoilerText = spoilerText == null ? null : replaceCrashingCharacters(spoilerText).toString(); this.spoilerText = spoilerText == null ? null : replaceCrashingCharacters(spoilerText).toString();
@ -212,7 +219,9 @@ public abstract class StatusViewData {
return isShowingContent; return isShowingContent;
} }
public boolean isBot(){ return isBot; } public boolean isBot() {
return isBot;
}
@Nullable @Nullable
public String getRebloggedAvatar() { public String getRebloggedAvatar() {
@ -318,7 +327,8 @@ public abstract class StatusViewData {
return poll; return poll;
} }
@Override public long getViewDataId() { @Override
public long getViewDataId() {
// Chance of collision is super low and impact of mistake is low as well // Chance of collision is super low and impact of mistake is low as well
return id.hashCode(); return id.hashCode();
} }
@ -341,8 +351,8 @@ public abstract class StatusViewData {
} }
public boolean deepEquals(StatusViewData o) { public boolean deepEquals(StatusViewData o) {
if (this == o) return true; if(this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if(o == null || getClass() != o.getClass()) return false;
Concrete concrete = (Concrete) o; Concrete concrete = (Concrete) o;
return reblogged == concrete.reblogged && return reblogged == concrete.reblogged &&
favourited == concrete.favourited && favourited == concrete.favourited &&
@ -393,17 +403,17 @@ public abstract class StatusViewData {
SpannableStringBuilder builder = null; SpannableStringBuilder builder = null;
int length = content.length(); int length = content.length();
for (int index = 0; index < length; ++index) { for(int index = 0; index < length; ++index) {
char character = content.charAt(index); char character = content.charAt(index);
// If there are more than one or two, switch to a map // If there are more than one or two, switch to a map
if (character == SOFT_HYPHEN) { if(character == SOFT_HYPHEN) {
if (!replacing) { if(!replacing) {
replacing = true; replacing = true;
builder = new SpannableStringBuilder(content, 0, index); builder = new SpannableStringBuilder(content, 0, index);
} }
builder.append(ASCII_HYPHEN); builder.append(ASCII_HYPHEN);
} else if (replacing) { } else if(replacing) {
builder.append(character); builder.append(character);
} }
} }
@ -429,19 +439,22 @@ public abstract class StatusViewData {
return id; return id;
} }
@Override public long getViewDataId() { @Override
public long getViewDataId() {
return id.hashCode(); return id.hashCode();
} }
@Override public boolean deepEquals(StatusViewData other) { @Override
if (!(other instanceof Placeholder)) return false; public boolean deepEquals(StatusViewData other) {
if(!(other instanceof Placeholder)) return false;
Placeholder that = (Placeholder) other; Placeholder that = (Placeholder) other;
return isLoading == that.isLoading && id.equals(that.id); return isLoading == that.isLoading && id.equals(that.id);
} }
@Override public boolean equals(Object o) { @Override
if (this == o) return true; public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false; if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Placeholder that = (Placeholder) o; Placeholder that = (Placeholder) o;
@ -486,14 +499,20 @@ public abstract class StatusViewData {
private List<Emoji> accountEmojis; private List<Emoji> accountEmojis;
private List<Emoji> rebloggedByAccountEmojis; private List<Emoji> rebloggedByAccountEmojis;
private Card card; private Card card;
private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */ private boolean isCollapsible;
private boolean isCollapsed; /** Whether the status is shown partially or fully */ /**
* Whether the status meets the requirement to be collapsed
*/
private boolean isCollapsed;
/**
* Whether the status is shown partially or fully
*/
private PollViewData poll; private PollViewData poll;
private boolean isBot; private boolean isBot;
private boolean isMuted; private boolean isMuted;
private boolean isThreadMuted; private boolean isThreadMuted;
private boolean isUserMuted; private boolean isUserMuted;
private int conversationId; private String conversationId;
private List<EmojiReaction> emojiReactions; private List<EmojiReaction> emojiReactions;
private boolean parentVisible; private boolean parentVisible;
@ -739,9 +758,9 @@ public abstract class StatusViewData {
return this; return this;
} }
public Builder setConversationId(int conversationId) { public Builder setConversationId(String conversationId) {
this.conversationId = conversationId; this.conversationId = conversationId;
return this; return this;
} }
public Builder setEmojiReactions(List<EmojiReaction> emojiReactions) { public Builder setEmojiReactions(List<EmojiReaction> emojiReactions) {
@ -750,9 +769,9 @@ public abstract class StatusViewData {
} }
public StatusViewData.Concrete createStatusViewData() { public StatusViewData.Concrete createStatusViewData() {
if (this.statusEmojis == null) statusEmojis = Collections.emptyList(); if(this.statusEmojis == null) statusEmojis = Collections.emptyList();
if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); if(this.accountEmojis == null) accountEmojis = Collections.emptyList();
if (this.createdAt == null) createdAt = new Date(); if(this.createdAt == null) createdAt = new Date();
return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText, return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText,
visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,