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:
parent
06b01e5a37
commit
0bd5182b9e
6 changed files with 203 additions and 150 deletions
|
@ -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?
|
||||||
)
|
)
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue