3771: Add an "in reply to" text to a reply

This commit is contained in:
Lakoja 2023-06-28 13:56:59 +02:00
parent fe7103f2b9
commit e6bbf9043b
24 changed files with 194 additions and 63 deletions

View File

@ -194,7 +194,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter<RecyclerView.View
if (payloads == null) {
holder.showStatusContent(true);
}
holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder);
holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder, true);
}
if (concreteNotification.getType() == Notification.Type.POLL) {
holder.setPollInfo(accountId.equals(concreteNotification.getAccount().getId()));

View File

@ -769,14 +769,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
public void setupWithStatus(@NonNull StatusViewData.Concrete status, final @NonNull StatusActionListener listener,
@NonNull StatusDisplayOptions statusDisplayOptions) {
this.setupWithStatus(status, listener, statusDisplayOptions, null);
@NonNull StatusDisplayOptions statusDisplayOptions, boolean showStatusInfo) {
this.setupWithStatus(status, listener, statusDisplayOptions, null, showStatusInfo);
}
public void setupWithStatus(@NonNull StatusViewData.Concrete status,
@NonNull final StatusActionListener listener,
@NonNull StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
@Nullable Object payloads,
boolean showStatusInfo) {
if (payloads == null) {
Status actionable = status.getActionable();
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);

View File

@ -143,13 +143,14 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
public void setupWithStatus(@NonNull final StatusViewData.Concrete status,
@NonNull final StatusActionListener listener,
@NonNull StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
@Nullable Object payloads,
boolean showStatusInfo) {
// We never collapse statuses in the detail view
StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ?
status.copyWithCollapsed(false) :
status;
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads);
super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads, showStatusInfo);
setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
if (payloads == null) {
Status actionable = uncollapsedStatus.getActionable();

View File

@ -30,6 +30,7 @@ import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.TimelineAccount;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.NumberUtils;
@ -38,6 +39,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.Collections;
import java.util.List;
import at.connyduck.sparkbutton.helpers.Utils;
@ -63,21 +65,37 @@ public class StatusViewHolder extends StatusBaseViewHolder {
public void setupWithStatus(@NonNull StatusViewData.Concrete status,
@NonNull final StatusActionListener listener,
@NonNull StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
@Nullable Object payloads,
boolean showStatusInfo) {
if (payloads == null) {
boolean sensitive = !TextUtils.isEmpty(status.getActionable().getSpoilerText());
boolean expanded = status.isExpanded();
setupCollapsedState(sensitive, expanded, status, listener);
Status reblogging = status.getRebloggingStatus();
if (reblogging == null || status.getFilterAction() == Filter.Action.WARN) {
boolean isReply = status.getStatus().getInReplyToId() != null;
boolean isReplyOnly = isReply && reblogging == null;
boolean hasStatusContext = reblogging != null || isReply;
if (!hasStatusContext || !showStatusInfo || status.getFilterAction() == Filter.Action.WARN) {
hideStatusInfo();
} else {
String rebloggedByDisplayName = reblogging.getAccount().getName();
setRebloggedByDisplayName(rebloggedByDisplayName,
reblogging.getAccount().getEmojis(), statusDisplayOptions);
String accountName = "";
List<Emoji> emojis = Collections.emptyList();
if (reblogging != null) {
accountName = reblogging.getAccount().getName();
emojis = reblogging.getAccount().getEmojis();
} else if (isReply) {
TimelineAccount repliedTo = status.getInReplyToAccount();
if (repliedTo != null) {
accountName = repliedTo.getName();
emojis = repliedTo.getEmojis();
}
}
setStatusInfoText(isReplyOnly, accountName, emojis, statusDisplayOptions);
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
}
@ -88,19 +106,27 @@ public class StatusViewHolder extends StatusBaseViewHolder {
setFavouritedCount(status.getActionable().getFavouritesCount());
setReblogsCount(status.getActionable().getReblogsCount());
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
super.setupWithStatus(status, listener, statusDisplayOptions, payloads, showStatusInfo);
}
private void setRebloggedByDisplayName(final CharSequence name,
final List<Emoji> accountEmoji,
final StatusDisplayOptions statusDisplayOptions) {
private void setStatusInfoText(final boolean isReply,
final CharSequence name,
final List<Emoji> accountEmoji,
final StatusDisplayOptions statusDisplayOptions) {
Context context = statusInfo.getContext();
CharSequence wrappedName = StringUtils.unicodeWrap(name);
CharSequence boostedText = context.getString(R.string.post_boosted_format, wrappedName);
CharSequence emojifiedText = CustomEmojiHelper.emojify(
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
);
statusInfo.setText(emojifiedText);
if (name.length() > 0) {
CharSequence wrappedName = StringUtils.unicodeWrap(name);
CharSequence statusContextText = context.getString(isReply ? R.string.post_replied_format : R.string.post_boosted_format, wrappedName);
CharSequence emojifiedText = CustomEmojiHelper.emojify(
statusContextText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
);
statusInfo.setText(emojifiedText);
} else {
statusInfo.setText(context.getString(R.string.post_replied));
}
statusInfo.setCompoundDrawablesWithIntrinsicBounds(isReply ? R.drawable.ic_reply_all_18dp : R.drawable.ic_reblog_18dp, 0, 0, 0);
statusInfo.setVisibility(View.VISIBLE);
}

View File

@ -135,6 +135,8 @@ data class ConversationStatusEntity(
language = language,
filtered = emptyList()
),
// TODO? implementation gap: not needed here atm, but inconsistent
inReplyToAccount = null,
isExpanded = expanded,
isShowingContent = showingHiddenContent,
isCollapsed = collapsed

View File

@ -38,7 +38,7 @@ class SearchStatusesAdapter(
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
getItem(position)?.let { item ->
holder.setupWithStatus(item, statusListener, statusDisplayOptions)
holder.setupWithStatus(item, statusListener, statusDisplayOptions, true)
}
}

View File

@ -90,7 +90,8 @@ class TimelinePagingAdapter(
status,
statusListener,
statusDisplayOptions,
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null,
true
)
}
}

View File

@ -206,8 +206,8 @@ fun TimelineStatusWithAccount.toViewData(moshi: Moshi, isDetailed: Boolean = fal
// no url for reblogs
url = null,
account = this.reblogAccount!!.toAccount(moshi),
inReplyToId = null,
inReplyToAccountId = null,
inReplyToId = status.inReplyToId,
inReplyToAccountId = status.inReplyToAccountId,
reblog = reblog,
content = "",
// lie but whatever?
@ -269,6 +269,7 @@ fun TimelineStatusWithAccount.toViewData(moshi: Moshi, isDetailed: Boolean = fal
}
return StatusViewData.Concrete(
status = status,
inReplyToAccount = this.inReplyToAccount?.toAccount(moshi),
isExpanded = this.status.expanded,
isShowingContent = this.status.contentShowing,
isCollapsed = this.status.contentCollapsed,

View File

@ -20,19 +20,27 @@ import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import com.keylesspalace.tusky.components.timeline.toAccount
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.toViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
import com.squareup.moshi.Moshi
import retrofit2.HttpException
@OptIn(ExperimentalPagingApi::class)
class NetworkTimelineRemoteMediator(
private val accountManager: AccountManager,
private val viewModel: NetworkTimelineViewModel
private val viewModel: NetworkTimelineViewModel,
db: AppDatabase,
private val moshi: Moshi,
) : RemoteMediator<String, StatusViewData>() {
private val accountDao = db.timelineAccountDao()
private val statusIds = mutableSetOf<String>()
init {
@ -80,7 +88,13 @@ class NetworkTimelineRemoteMediator(
val expanded = oldStatus?.isExpanded ?: activeAccount.alwaysOpenSpoiler
val contentCollapsed = oldStatus?.isCollapsed ?: true
var inReplyToAccount: TimelineAccount? = null
if (status.inReplyToAccountId != null) {
inReplyToAccount = accountDao.get(status.inReplyToAccountId)?.toAccount(moshi)
}
status.toViewData(
inReplyToAccount = inReplyToAccount,
isShowingContent = contentShowing,
isExpanded = expanded,
isCollapsed = contentCollapsed

View File

@ -29,6 +29,7 @@ import at.connyduck.calladapter.networkresult.onFailure
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.components.timeline.util.ifExpected
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
@ -41,6 +42,7 @@ import com.keylesspalace.tusky.util.isLessThanOrEqual
import com.keylesspalace.tusky.util.toViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
import com.keylesspalace.tusky.viewdata.TranslationViewData
import com.squareup.moshi.Moshi
import java.io.IOException
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
@ -60,7 +62,9 @@ class NetworkTimelineViewModel @Inject constructor(
eventHub: EventHub,
accountManager: AccountManager,
sharedPreferences: SharedPreferences,
filterModel: FilterModel
filterModel: FilterModel,
private val db: AppDatabase,
private val moshi: Moshi,
) : TimelineViewModel(
timelineCases,
api,
@ -86,7 +90,7 @@ class NetworkTimelineViewModel @Inject constructor(
currentSource = source
}
},
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this)
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this, db, moshi)
).flow
.map { pagingData ->
pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData ->

View File

@ -53,7 +53,7 @@ class ThreadAdapter(
override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) {
val status = getItem(position)
viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions)
viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions, false)
}
override fun getItemViewType(position: Int): Int {

View File

@ -61,6 +61,7 @@ public abstract class AppDatabase extends RoomDatabase {
@NonNull public abstract ConversationsDao conversationDao();
@NonNull public abstract TimelineDao timelineDao();
@NonNull public abstract DraftDao draftDao();
@NonNull public abstract TimelineAccountDao timelineAccountDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override

View File

@ -0,0 +1,25 @@
/* Copyright 2023 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.db
import androidx.room.Dao
import androidx.room.Query
@Dao
interface TimelineAccountDao {
@Query("SELECT * FROM TimelineAccountEntity WHERE serverId = :id")
suspend fun get(id: String): TimelineAccountEntity?
}

View File

@ -44,17 +44,22 @@ s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, s.language, s.filtered,
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
a.localUsername as 'a_localUsername', a.username as 'a_username',
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
a.emojis as 'a_emojis', a.bot as 'a_bot',
rb.serverId as 'rb_serverId', rb.timelineUserId 'rb_timelineUserId',
rb.localUsername as 'rb_localUsername', rb.username as 'rb_username',
rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar',
rb.emojis as 'rb_emojis', rb.bot as 'rb_bot'
author.serverId as 'author_serverId', author.timelineUserId as 'author_timelineUserId',
author.localUsername as 'author_localUsername', author.username as 'author_username',
author.displayName as 'author_displayName', author.url as 'author_url', author.avatar as 'author_avatar',
author.emojis as 'author_emojis', author.bot as 'author_bot',
replied.serverId as 'replied_serverId', replied.timelineUserId 'replied_timelineUserId',
replied.localUsername as 'replied_localUsername', replied.username as 'replied_username',
replied.displayName as 'replied_displayName', replied.url as 'replied_url', replied.avatar as 'replied_avatar',
replied.emojis as 'replied_emojis', replied.bot as 'replied_bot',
reblogger.serverId as 'reblogger_serverId', reblogger.timelineUserId 'reblogger_timelineUserId',
reblogger.localUsername as 'reblogger_localUsername', reblogger.username as 'reblogger_username',
reblogger.displayName as 'reblogger_displayName', reblogger.url as 'reblogger_url', reblogger.avatar as 'reblogger_avatar',
reblogger.emojis as 'reblogger_emojis', reblogger.bot as 'reblogger_bot'
FROM TimelineStatusEntity s
LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId)
LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId)
LEFT JOIN TimelineAccountEntity author ON (s.timelineUserId = author.timelineUserId AND s.authorServerId = author.serverId)
LEFT JOIN TimelineAccountEntity replied ON (s.inReplyToAccountId = replied.serverId)
LEFT JOIN TimelineAccountEntity reblogger ON (s.timelineUserId = reblogger.timelineUserId AND s.reblogAccountId = reblogger.serverId)
WHERE s.timelineUserId = :account
ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC"""
)
@ -67,17 +72,22 @@ s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, s.language, s.filtered,
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
a.localUsername as 'a_localUsername', a.username as 'a_username',
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
a.emojis as 'a_emojis', a.bot as 'a_bot',
rb.serverId as 'rb_serverId', rb.timelineUserId 'rb_timelineUserId',
rb.localUsername as 'rb_localUsername', rb.username as 'rb_username',
rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar',
rb.emojis as 'rb_emojis', rb.bot as 'rb_bot'
author.serverId as 'author_serverId', author.timelineUserId as 'author_timelineUserId',
author.localUsername as 'author_localUsername', author.username as 'author_username',
author.displayName as 'author_displayName', author.url as 'author_url', author.avatar as 'author_avatar',
author.emojis as 'author_emojis', author.bot as 'author_bot',
replied.serverId as 'replied_serverId', replied.timelineUserId 'replied_timelineUserId',
replied.localUsername as 'replied_localUsername', replied.username as 'replied_username',
replied.displayName as 'replied_displayName', replied.url as 'replied_url', replied.avatar as 'replied_avatar',
replied.emojis as 'replied_emojis', replied.bot as 'replied_bot',
reblogger.serverId as 'reblogger_serverId', reblogger.timelineUserId 'reblogger_timelineUserId',
reblogger.localUsername as 'reblogger_localUsername', reblogger.username as 'reblogger_username',
reblogger.displayName as 'reblogger_displayName', reblogger.url as 'reblogger_url', reblogger.avatar as 'reblogger_avatar',
reblogger.emojis as 'reblogger_emojis', reblogger.bot as 'reblogger_bot'
FROM TimelineStatusEntity s
LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId)
LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId)
LEFT JOIN TimelineAccountEntity author ON (s.timelineUserId = author.timelineUserId AND s.authorServerId = author.serverId)
LEFT JOIN TimelineAccountEntity replied ON (s.inReplyToAccountId = replied.serverId)
LEFT JOIN TimelineAccountEntity reblogger ON (s.timelineUserId = reblogger.timelineUserId AND s.reblogAccountId = reblogger.serverId)
WHERE (s.serverId = :statusId OR s.reblogServerId = :statusId)
AND s.authorServerId IS NOT NULL
AND s.timelineUserId = :accountId"""

View File

@ -111,10 +111,16 @@ data class TimelineAccountEntity(
data class TimelineStatusWithAccount(
@Embedded
val status: TimelineStatusEntity,
// null when placeholder
@Embedded(prefix = "a_")
@Embedded(prefix = "author_")
val account: TimelineAccountEntity? = null,
// null when no reply
@Embedded(prefix = "replied_")
val inReplyToAccount: TimelineAccountEntity? = null,
// null when no reblog
@Embedded(prefix = "rb_")
@Embedded(prefix = "reblogger_")
val reblogAccount: TimelineAccountEntity? = null
)

View File

@ -38,6 +38,7 @@ import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.entity.TrendingTag
import com.keylesspalace.tusky.viewdata.NotificationViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
@ -49,10 +50,12 @@ fun Status.toViewData(
isExpanded: Boolean,
isCollapsed: Boolean,
isDetailed: Boolean = false,
inReplyToAccount: TimelineAccount? = null,
translation: TranslationViewData? = null,
): StatusViewData.Concrete {
return StatusViewData.Concrete(
status = this,
inReplyToAccount = inReplyToAccount,
isShowingContent = isShowingContent,
isCollapsed = isCollapsed,
isExpanded = isExpanded,
@ -71,6 +74,7 @@ fun Notification.toViewData(
this.type,
this.id,
this.account,
// TODO? account null implementation gap; and other locations:
this.status?.toViewData(isShowingContent, isExpanded, isCollapsed),
this.report
)

View File

@ -18,6 +18,7 @@ import android.text.Spanned
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.entity.Translation
import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.shouldTrimStatus
@ -45,6 +46,7 @@ sealed class StatusViewData {
data class Concrete(
val status: Status,
val inReplyToAccount: TimelineAccount?,
val isExpanded: Boolean,
val isShowingContent: Boolean,
/**

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:autoMirrored="true"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#fff"
android:pathData="M7,8L7,5l-7,7 7,7v-3l-4,-4 4,-4zM13,9L13,5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" />
</vector>

View File

@ -29,7 +29,8 @@
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlSymmetry"
tools:text="ConnyDuck boosted"
tools:visibility="visible" />
tools:visibility="visible"
app:drawableTint="?android:textColorTertiary" />
<ImageView
android:id="@+id/status_avatar"

View File

@ -84,6 +84,8 @@
<string name="post_username_format">\@%s</string>
<string name="post_boosted_format">%s boosted</string>
<string name="post_replied">Replied</string>
<string name="post_replied_format">In reply to %s</string>
<string name="post_sensitive_media_title">Sensitive content</string>
<string name="post_media_hidden_title">Media hidden</string>
<string name="post_media_alt">ALT</string>

View File

@ -44,12 +44,14 @@ class StatusComparisonTest {
fun `two equal status view data - should be equal`() {
val viewdata1 = StatusViewData.Concrete(
status = createStatus(),
inReplyToAccount = null,
isExpanded = false,
isShowingContent = false,
isCollapsed = false
)
val viewdata2 = StatusViewData.Concrete(
status = createStatus(),
inReplyToAccount = null,
isExpanded = false,
isShowingContent = false,
isCollapsed = false
@ -61,12 +63,14 @@ class StatusComparisonTest {
fun `status view data with different isExpanded - should not be equal`() {
val viewdata1 = StatusViewData.Concrete(
status = createStatus(),
inReplyToAccount = null,
isExpanded = true,
isShowingContent = false,
isCollapsed = false
)
val viewdata2 = StatusViewData.Concrete(
status = createStatus(),
inReplyToAccount = null,
isExpanded = false,
isShowingContent = false,
isCollapsed = false
@ -78,12 +82,14 @@ class StatusComparisonTest {
fun `status view data with different statuses- should not be equal`() {
val viewdata1 = StatusViewData.Concrete(
status = createStatus(content = "whatever"),
inReplyToAccount = null,
isExpanded = true,
isShowingContent = false,
isCollapsed = false
)
val viewdata2 = StatusViewData.Concrete(
status = createStatus(),
inReplyToAccount = null,
isExpanded = false,
isShowingContent = false,
isCollapsed = false
@ -104,7 +110,7 @@ class StatusComparisonTest {
"id": "$id",
"created_at": "2022-02-26T09:54:45.000Z",
"in_reply_to_id": null,
"in_reply_to_account_id": null,
"in_reply_to_account": null,
"sensitive": false,
"spoiler_text": "",
"visibility": "public",

View File

@ -12,7 +12,10 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineView
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.TimelineAccountDao
import com.keylesspalace.tusky.viewdata.StatusViewData
import com.squareup.moshi.Moshi
import java.io.IOException
import kotlinx.coroutines.runBlocking
import okhttp3.Headers
@ -21,6 +24,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
@ -45,6 +49,14 @@ class NetworkTimelineRemoteMediatorTest {
)
}
private val accountDao: TimelineAccountDao = mock {
onBlocking { get(any()) } doReturn null
}
private val db: AppDatabase = mock {
on { timelineAccountDao() } doReturn accountDao
}
private val moshi: Moshi = mock {}
@Test
@ExperimentalPagingApi
fun `should return error when network call returns error code`() {
@ -53,7 +65,7 @@ class NetworkTimelineRemoteMediatorTest {
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody())
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
@ -70,7 +82,7 @@ class NetworkTimelineRemoteMediatorTest {
onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException()
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) }
@ -99,7 +111,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val state = state(
listOf(
@ -146,7 +158,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val state = state(
listOf(
@ -198,7 +210,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val state = state(
listOf(
@ -251,7 +263,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val state = state(
listOf(
@ -308,7 +320,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val state = state(
listOf(
@ -354,7 +366,7 @@ class NetworkTimelineRemoteMediatorTest {
on { nextKey } doReturn null
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val state = state(
listOf(
@ -409,7 +421,7 @@ class NetworkTimelineRemoteMediatorTest {
)
}
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel)
val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel, db, moshi)
val state = state(
listOf(

View File

@ -79,6 +79,7 @@ fun mockStatusViewData(
favourited = favourited,
bookmarked = bookmarked
),
inReplyToAccount = null,
isExpanded = isExpanded,
isShowingContent = isShowingContent,
isCollapsed = isCollapsed,

View File

@ -108,6 +108,7 @@ glide-animation-plugin = { module = "com.github.penfeizhou.android.animation:gli
glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
glide-core = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
glide-okhttp3-integration = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" }
kapt = { module = "androidx.room:room-compiler", version.ref = "androidx-room" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
image-cropper = { module = "com.github.CanHub:Android-Image-Cropper", version.ref = "image-cropper" }