diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 179464543..8c6b570c0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -194,7 +194,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter 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 accountEmoji, - final StatusDisplayOptions statusDisplayOptions) { + private void setStatusInfoText(final boolean isReply, + final CharSequence name, + final List 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); } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index d38898c77..d23f52433 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt index 1d3cabe21..fee743746 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt @@ -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) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt index 716a30199..8f7615c15 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt @@ -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 ) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt index 4f8251d9e..e4afb7850 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -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, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt index f19b2240f..d66ac7933 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt @@ -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() { + private val accountDao = db.timelineAccountDao() + private val statusIds = mutableSetOf() 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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index 60eed28e0..eb2b78e85 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -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 -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt index 0c1c7fc5b..a5c314f1c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt @@ -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 { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 39261cb55..efa0093b2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineAccountDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineAccountDao.kt new file mode 100644 index 000000000..d2759627a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineAccountDao.kt @@ -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 . */ + +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? +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt index 3cd49baac..87a13c18b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -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""" diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt index ba32f6380..e241de41c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -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 ) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt index 0975b00f1..b3af8f61e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -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 ) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index 6f8d5d698..e744b4ca9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -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, /** diff --git a/app/src/main/res/drawable/ic_reply_all_18dp.xml b/app/src/main/res/drawable/ic_reply_all_18dp.xml new file mode 100644 index 000000000..9c46d7fd1 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_all_18dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index c022c2dba..31d107f2d 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -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" /> \@%s %s boosted + Replied + In reply to %s Sensitive content Media hidden ALT diff --git a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt index 6fd1c6a51..963394e3a 100644 --- a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt @@ -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", diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt index fe33c0933..3d779a2f2 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt @@ -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( diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt index 7bcd656fd..a4e4a49b5 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt @@ -79,6 +79,7 @@ fun mockStatusViewData( favourited = favourited, bookmarked = bookmarked ), + inReplyToAccount = null, isExpanded = isExpanded, isShowingContent = isShowingContent, isCollapsed = isCollapsed, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b0da4bc8..22d28ceca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }