3771: Add an "in reply to" text to a reply
This commit is contained in:
parent
fe7103f2b9
commit
e6bbf9043b
|
@ -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()));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
}
|
|
@ -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"""
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
/**
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -79,6 +79,7 @@ fun mockStatusViewData(
|
|||
favourited = favourited,
|
||||
bookmarked = bookmarked
|
||||
),
|
||||
inReplyToAccount = null,
|
||||
isExpanded = isExpanded,
|
||||
isShowingContent = isShowingContent,
|
||||
isCollapsed = isCollapsed,
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in New Issue