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
* 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 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.
* 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
* 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.entity
@ -19,7 +24,7 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.URLSpan
import com.google.gson.annotations.SerializedName
import java.util.*
import java.util.Date
data class Status(
var id: String,
@ -58,17 +63,21 @@ data class Status(
enum class Visibility(val num: Int) {
UNKNOWN(0),
@SerializedName("public")
PUBLIC(1),
@SerializedName("unlisted")
UNLISTED(2),
@SerializedName("private")
PRIVATE(3),
@SerializedName("direct")
DIRECT(4);
fun serverString(): String {
return when (this) {
return when(this) {
PUBLIC -> "public"
UNLISTED -> "unlisted"
PRIVATE -> "private"
@ -81,7 +90,7 @@ data class Status(
@JvmStatic
fun byNum(num: Int): Visibility {
return when (num) {
return when(num) {
4 -> DIRECT
3 -> PRIVATE
2 -> UNLISTED
@ -93,7 +102,7 @@ data class Status(
@JvmStatic
fun byString(s: String): Visibility {
return when (s) {
return when(s) {
"public" -> PUBLIC
"unlisted" -> UNLISTED
"private" -> PRIVATE
@ -143,8 +152,8 @@ data class Status(
pleroma.threadMuted = mute
}
fun getConversationId(): Int {
return pleroma?.conversationId ?: -1
fun getConversationId(): String {
return pleroma?.conversationId ?: ""
}
fun getEmojiReactions(): List<EmojiReaction>? {
@ -161,10 +170,10 @@ data class Status(
private fun getEditableText(): String {
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
for ((_, url1, username) in mentions) {
if (url == url1) {
for((_, url1, username) in mentions) {
if(url == url1) {
val start = builder.getSpanStart(span)
val end = builder.getSpanEnd(span)
builder.replace(start, end, "@$username")
@ -176,8 +185,8 @@ data class Status(
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
if(this === other) return true
if(other == null || javaClass != other.javaClass) return false
val status = other as Status?
return id == status?.id
@ -189,20 +198,20 @@ data class Status(
data class PleromaStatus(
@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("in_reply_to_account_acct") val inReplyToAccountAcct: String?,
@SerializedName("parent_visible") val parentVisible: Boolean?
)
data class Mention (
data class Mention(
val id: String,
val url: String?, // can be null due to bug in some Pleroma versions
@SerializedName("acct") val username: String,
@SerializedName("username") val localUsername: String
)
data class Application (
data class Application(
val name: String,
val website: String?
)

View file

@ -350,9 +350,9 @@ public class NotificationsFragment extends SFragment implements
if (posAndNotification == null)
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) {
setMutedStatusForStatus(posAndNotification.first, posAndNotification.second.getStatus(), event.getMute(), event.getMute());
} else {
@ -1016,7 +1016,7 @@ public class NotificationsFragment extends SFragment implements
updateAdapter();
}
private void removeAllByConversationId(int conversationId) {
private void removeAllByConversationId(String conversationId) {
// using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
while (iterator.hasNext()) {
@ -1024,7 +1024,7 @@ public class NotificationsFragment extends SFragment implements
Notification notification = placeholderOrNotification.asRightOrNull();
if (notification != null && notification.getStatus() != null
&& notification.getType() == Notification.Type.MENTION &&
notification.getStatus().getConversationId() == conversationId) {
notification.getStatus().getConversationId().equalsIgnoreCase(conversationId)) {
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
* 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 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.
* 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
* 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.fragment;
@ -22,7 +28,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -108,13 +113,13 @@ import kotlin.jvm.functions.Function1;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import timber.log.Timber;
public class TimelineFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
Injectable, ReselectableFragment, RefreshableFragment {
private static final String TAG = "TimelineF"; // logging tag
private static final String KIND_ARG = "kind";
private static final String ID_ARG = "id";
private static final String HASHTAGS_ARG = "hastags";
@ -475,7 +480,7 @@ public class TimelineFragment extends SFragment implements
public void onActivityCreated(@Nullable Bundle 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
* guaranteed to be set until then. */
@ -623,7 +628,7 @@ public class TimelineFragment extends SFragment implements
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
(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)))
.subscribe(
(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)))
.subscribe(
(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)))
.subscribe(
(newPoll) -> setVoteForPoll(position, status, newPoll),
(t) -> Log.d(TAG,
"Failed to vote in poll: " + status.getId(), t)
(t) -> Timber.e(t,
"Failed to vote in poll: " + status.getId())
);
}
@ -815,7 +820,7 @@ public class TimelineFragment extends SFragment implements
? statuses.get(position + 1).asRight().getId()
: 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;
}
sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), maxMinusOne,
@ -826,14 +831,14 @@ public class TimelineFragment extends SFragment implements
statuses.setPairedItem(position, newViewData);
updateAdapter();
} else {
Log.e(TAG, "error loading more");
Timber.e("error loading more");
}
}
@Override
public void onContentCollapsedChange(boolean isCollapsed, int position) {
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;
}
@ -841,7 +846,7 @@ public class TimelineFragment extends SFragment implements
if(!(status instanceof StatusViewData.Concrete)) {
// 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.
Log.e(TAG, String.format(
Timber.e(String.format(
"Expected StatusViewData.Concrete, got %s instead at position: %d of %d",
status == null ? "<null>" : status.getClass().getSimpleName(),
position,
@ -960,13 +965,13 @@ public class TimelineFragment extends SFragment implements
updateAdapter();
}
private void removeAllByConversationId(int conversationId) {
private void removeAllByConversationId(String conversationId) {
// using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
while(iterator.hasNext()) {
Status status = iterator.next().asRightOrNull();
if(status != null &&
(status.getConversationId() == conversationId) || status.getActionableStatus().getConversationId() == conversationId) {
(status.getConversationId().equalsIgnoreCase(conversationId)) || status.getActionableStatus().getConversationId().equalsIgnoreCase(conversationId)) {
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);
progressBar.setVisibility(View.GONE);
}
@ -1453,9 +1458,9 @@ public class TimelineFragment extends SFragment implements
return;
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()) {
statuses.remove(pos);
} else {
@ -1650,13 +1655,12 @@ public class TimelineFragment extends SFragment implements
.as(autoDisposable(from(this)))
.subscribe(
(newStatus) -> setEmojiReactionForStatus(position, newStatus),
(t) -> Log.d(TAG,
"Failed to react with " + emoji + " on status: " + statusId, t)
(t) -> Timber.e(t,
"Failed to react with " + emoji + " on status: " + statusId)
);
}
@Override
public void onEmojiReactMenu(@NonNull View view, final EmojiReaction emoji, final String statusId) {
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
import android.text.SpannedString

View file

@ -53,6 +53,7 @@ object OkHttpUtils {
val builder = OkHttpClient.Builder()
.addInterceptor(getUserAgentInterceptor())
//.addInterceptor(getDebugInformation())
.addInterceptor(BrotliInterceptor)
.readTimeout(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
* 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 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.
* 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
* 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.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.EmojiReaction;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Status;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -36,15 +38,14 @@ 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() { }
private StatusViewData() {
}
public abstract long getViewDataId();
@ -91,15 +92,21 @@ public abstract class StatusViewData {
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 */
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;
private final boolean isMuted; /* user toggle */
private final boolean isThreadMuted; /* thread_muted state got from backend */
private final boolean isUserMuted; /* muted state got from backend */
private final int conversationId;
private final String conversationId;
@Nullable
private final List<EmojiReaction> emojiReactions;
private final boolean parentVisible;
@ -112,10 +119,10 @@ public abstract class StatusViewData {
@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,
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;
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
this.content = replaceCrashingCharacters(content);
this.spoilerText = spoilerText == null ? null : replaceCrashingCharacters(spoilerText).toString();
@ -212,7 +219,9 @@ public abstract class StatusViewData {
return isShowingContent;
}
public boolean isBot(){ return isBot; }
public boolean isBot() {
return isBot;
}
@Nullable
public String getRebloggedAvatar() {
@ -318,7 +327,8 @@ public abstract class StatusViewData {
return poll;
}
@Override public long getViewDataId() {
@Override
public long getViewDataId() {
// Chance of collision is super low and impact of mistake is low as well
return id.hashCode();
}
@ -341,8 +351,8 @@ public abstract class StatusViewData {
}
public boolean deepEquals(StatusViewData o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Concrete concrete = (Concrete) o;
return reblogged == concrete.reblogged &&
favourited == concrete.favourited &&
@ -393,17 +403,17 @@ public abstract class StatusViewData {
SpannableStringBuilder builder = null;
int length = content.length();
for (int index = 0; index < length; ++index) {
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) {
if(character == SOFT_HYPHEN) {
if(!replacing) {
replacing = true;
builder = new SpannableStringBuilder(content, 0, index);
}
builder.append(ASCII_HYPHEN);
} else if (replacing) {
} else if(replacing) {
builder.append(character);
}
}
@ -429,19 +439,22 @@ public abstract class StatusViewData {
return id;
}
@Override public long getViewDataId() {
@Override
public long getViewDataId() {
return id.hashCode();
}
@Override public boolean deepEquals(StatusViewData other) {
if (!(other instanceof Placeholder)) return false;
@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;
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Placeholder that = (Placeholder) o;
@ -486,14 +499,20 @@ public abstract class StatusViewData {
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 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;
private boolean isMuted;
private boolean isThreadMuted;
private boolean isUserMuted;
private int conversationId;
private String conversationId;
private List<EmojiReaction> emojiReactions;
private boolean parentVisible;
@ -739,7 +758,7 @@ public abstract class StatusViewData {
return this;
}
public Builder setConversationId(int conversationId) {
public Builder setConversationId(String conversationId) {
this.conversationId = conversationId;
return this;
}
@ -750,9 +769,9 @@ public abstract class StatusViewData {
}
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();
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, spoilerText,
visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,