Display thread header in CFv2.

This commit is contained in:
Cody Henthorne
2023-05-11 15:39:59 -04:00
committed by Greyson Parrelli
parent ffbbdc1576
commit 3ba128793a
26 changed files with 235 additions and 49 deletions

View File

@@ -115,5 +115,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
}
}

View File

@@ -19,7 +19,7 @@ class CallLogPagedDataSource(
return callsCount + hasFilter.toInt() + hasCallLinkRow.toInt()
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<CallLogRow> {
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<CallLogRow> {
val calls = mutableListOf<CallLogRow>()
val callLimit = length - hasCallLinkRow.toInt()

View File

@@ -74,7 +74,7 @@ class ContactSearchPagedDataSource(
return searchSize
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ContactSearchData> {
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ContactSearchData> {
val sections: List<ContactSearchConfiguration.Section> = if (displayEmptyState) {
contactConfiguration.emptyStateSections
} else {

View File

@@ -37,7 +37,6 @@ import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
@@ -99,7 +98,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
}
@Override
public @NonNull List<ConversationMessage> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
public @NonNull List<ConversationMessage> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
List<MessageRecord> records = new ArrayList<>(length);
MentionHelper mentionHelper = new MentionHelper();
@@ -128,11 +127,11 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
}
}
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= size())) {
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= totalSize)) {
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
}
if (messageRequestData.isHidden() && (start + length >= size())) {
if (messageRequestData.isHidden() && (start + length >= totalSize)) {
records.add(new InMemoryMessageRecord.RemovedContactHidden(threadId));
}

View File

@@ -89,7 +89,6 @@ import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment;
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
@@ -194,7 +193,6 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import java.io.IOException;
@@ -2065,6 +2063,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
}
@Override
public void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks) {
GroupDescriptionDialog.show(getChildFragmentManager(), groupName, description, shouldLinkifyWebLinks);
}
@Override
public void onActivatePaymentsClicked() {
Intent intent = new Intent(requireContext(), PaymentsActivity.class);

View File

@@ -437,7 +437,7 @@ public class ConversationIntents {
}
private static long resolveThreadId(@NonNull RecipientId recipientId, long threadId) {
if (threadId >= 0 && SignalStore.internalValues().useConversationFragmentV2()) {
if (threadId < 0 && SignalStore.internalValues().useConversationFragmentV2()) {
Log.w(TAG, "Getting thread id from database...");
// TODO [alex] -- Yes, this hits the database. No, we shouldn't be doing this.
return SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(recipientId));

View File

@@ -5,8 +5,10 @@
package org.thoughtcrime.securesms.conversation.v2
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import androidx.core.text.HtmlCompat
import androidx.lifecycle.LifecycleOwner
import com.google.android.exoplayer2.MediaItem
import org.signal.core.util.logging.Log
@@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.BindableConversationItem
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.ConversationAdapter
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
import org.thoughtcrime.securesms.conversation.ConversationBannerView
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizable
@@ -27,11 +30,17 @@ import org.thoughtcrime.securesms.conversation.v2.data.IncomingMedia
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingMedia
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
import org.thoughtcrime.securesms.conversation.v2.data.ThreadHeader
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.CachedInflater
import org.thoughtcrime.securesms.util.HtmlUtil
import org.thoughtcrime.securesms.util.Projection
import org.thoughtcrime.securesms.util.ProjectionList
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
@@ -65,6 +74,8 @@ class ConversationAdapterV2(
private val condensedMode: ConversationItemDisplayMode? = null
init {
registerFactory(ThreadHeader::class.java, ::ThreadHeaderViewHolder, R.layout.conversation_item_banner)
registerFactory(ConversationUpdate::class.java) { parent ->
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_update, parent, false)
ConversationUpdateViewHolder(view)
@@ -91,14 +102,14 @@ class ConversationAdapterV2(
}
}
fun getAdapterPositionForMessagePosition(startPosition: Int): Int {
return startPosition - 1
/** [messagePosition] is one-based index and adapter is zero-based. */
fun getAdapterPositionForMessagePosition(messagePosition: Int): Int {
return messagePosition - 1
}
fun getLastVisibleConversationMessage(position: Int): ConversationMessage? {
return try {
// todo [cody] handle conversation banner adjustment
getConversationMessage(position)
getConversationMessage(position) ?: getConversationMessage(position - 1)
} catch (e: IndexOutOfBoundsException) {
Log.w(TAG, "Race condition changed size of conversation", e)
null
@@ -131,6 +142,7 @@ class ConversationAdapterV2(
override fun getConversationMessage(position: Int): ConversationMessage? {
return when (val item = getItem(position)) {
is ConversationMessageElement -> item.conversationMessage
is ThreadHeader -> null
null -> null
else -> throw AssertionError("Invalid item: ${item.javaClass}")
}
@@ -326,4 +338,75 @@ class ConversationAdapterV2(
return bindable.getColorizerProjections(coordinateRoot)
}
}
inner class ThreadHeaderViewHolder(itemView: View) : MappingViewHolder<ThreadHeader>(itemView) {
private val conversationBanner: ConversationBannerView = itemView as ConversationBannerView
override fun bind(model: ThreadHeader) {
val (recipient, groupInfo, sharedGroups, messageRequestState) = model.recipientInfo
val isSelf = recipient.id == Recipient.self().id
conversationBanner.setAvatar(glideRequests, recipient)
conversationBanner.showBackgroundBubble(recipient.hasWallpaper())
val title: String = conversationBanner.setTitle(recipient)
conversationBanner.setAbout(recipient)
if (recipient.isGroup) {
if (groupInfo.pendingMemberCount > 0) {
val invited = context.resources.getQuantityString(R.plurals.MessageRequestProfileView_invited, groupInfo.pendingMemberCount, groupInfo.pendingMemberCount)
conversationBanner.setSubtitle(context.resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, groupInfo.fullMemberCount, groupInfo.fullMemberCount, invited))
} else if (groupInfo.fullMemberCount > 0) {
conversationBanner.setSubtitle(context.resources.getQuantityString(R.plurals.MessageRequestProfileView_members, groupInfo.fullMemberCount, groupInfo.fullMemberCount))
} else {
conversationBanner.setSubtitle(null)
}
} else if (isSelf) {
conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation))
} else {
val subtitle: String? = recipient.e164.map { e164: String? -> PhoneNumberFormatter.prettyPrint(e164!!) }.orElse(null)
if (subtitle == null || subtitle == title) {
conversationBanner.hideSubtitle()
} else {
conversationBanner.setSubtitle(subtitle)
}
}
if (sharedGroups.isEmpty() || isSelf) {
if (TextUtils.isEmpty(groupInfo.description)) {
conversationBanner.setLinkifyDescription(false)
conversationBanner.hideDescription()
} else {
conversationBanner.setLinkifyDescription(true)
val linkifyWebLinks = messageRequestState == MessageRequestState.NONE
conversationBanner.showDescription()
GroupDescriptionUtil.setText(
context,
conversationBanner.description,
groupInfo.description,
linkifyWebLinks
) {
clickListener.onShowGroupDescriptionClicked(recipient.getDisplayName(context), groupInfo.description, linkifyWebLinks)
}
}
} else {
val description: String = when (sharedGroups.size) {
1 -> context.getString(R.string.MessageRequestProfileView_member_of_one_group, HtmlUtil.bold(sharedGroups[0]))
2 -> context.getString(R.string.MessageRequestProfileView_member_of_two_groups, HtmlUtil.bold(sharedGroups[0]), HtmlUtil.bold(sharedGroups[1]))
3 -> context.getString(R.string.MessageRequestProfileView_member_of_many_groups, HtmlUtil.bold(sharedGroups[0]), HtmlUtil.bold(sharedGroups[1]), HtmlUtil.bold(sharedGroups[2]))
else -> {
val others: Int = sharedGroups.size - 2
context.getString(
R.string.MessageRequestProfileView_member_of_many_groups,
HtmlUtil.bold(sharedGroups[0]),
HtmlUtil.bold(sharedGroups[1]),
context.resources.getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, others, others)
)
}
}
conversationBanner.setDescription(HtmlCompat.fromHtml(description, 0))
conversationBanner.showDescription()
}
}
}
}

View File

@@ -247,15 +247,16 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
.flatMapObservable { it.items.data }
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onNext = {
if (firstRender) {
SignalLocalMetrics.ConversationOpen.onDataPostedToMain()
}
adapter.submitList(it) {
scrollToPositionDelegate.notifyListCommitted()
if (firstRender) {
binding.conversationItemRecycler.doAfterNextLayout {
SignalLocalMetrics.ConversationOpen.onRenderFinished()
if (firstRender) {
firstRender = false
doAfterFirstRender()
animationsAllowed = true
@@ -891,6 +892,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
// TODO [alex] -- ("Not yet implemented")
}
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) {
GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks)
}
}
private inner class ConversationOptionsMenuCallback : ConversationOptionsMenu.Callback {

View File

@@ -17,6 +17,7 @@ class ConversationRecipientRepository(threadId: Long) {
.flatMapObservable { Recipient.observable(it) }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.distinctUntilChanged { previous, next -> previous === next || previous.hasSameContent(next) }
.replay(1)
.refCount()
.observeOn(Schedulers.io())

View File

@@ -1,3 +1,8 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
@@ -29,9 +34,12 @@ class ConversationRepository(context: Context) {
*/
fun getConversationThreadState(threadId: Long, requestedStartPosition: Int): Single<ConversationThreadState> {
return Single.fromCallable {
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted()
val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)!!
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted()
val metadata = oldConversationRepository.getConversationData(threadId, recipient, requestedStartPosition)
SignalLocalMetrics.ConversationOpen.onMetadataLoaded()
val messageRequestData = metadata.messageRequestData
val dataSource = ConversationDataSource(
applicationContext,
@@ -48,9 +56,7 @@ class ConversationRepository(context: Context) {
ConversationThreadState(
items = PagedData.createForObservable(dataSource, config),
meta = metadata
).apply {
SignalLocalMetrics.ConversationOpen.onMetadataLoaded()
}
)
}.subscribeOn(Schedulers.io())
}

View File

@@ -79,6 +79,11 @@ class ConversationViewModel(
_recipient.onNext(it)
})
disposables += recipientRepository
.conversationRecipient
.skip(1) // We can safely skip the first emission since this is used for updating the header on future changes
.subscribeBy { pagingController.onDataItemChanged(ConversationElementKey.threadHeader) }
disposables += repository.getConversationThreadState(threadId, requestedStartingPosition)
.subscribeBy(onSuccess = {
pagingController.set(it.items.controller)

View File

@@ -12,10 +12,10 @@ import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.concurrent.subscribeWithSubject
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.messagerequests.GroupInfo
import org.thoughtcrime.securesms.messagerequests.MessageRequestRecipientInfo
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.MessageData
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.RecipientInfo
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.RequestReviewDisplayState
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.Status
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
@@ -70,12 +70,12 @@ class MessageRequestViewModel(
}
}.subscribeWithSubject(BehaviorSubject.create(), disposables)
val recipientInfo: Observable<RecipientInfo> = Observable.combineLatest(
val recipientInfo: Observable<MessageRequestRecipientInfo> = Observable.combineLatest(
recipientRepository.conversationRecipient,
groupInfo,
groups,
messageDataSubject.map { it.messageState },
::RecipientInfo
::MessageRequestRecipientInfo
)
override fun onCleared() {

View File

@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -33,10 +34,12 @@ private typealias ConversationElement = MappingModel<*>
sealed interface ConversationElementKey {
companion object {
fun forMessage(id: Long): ConversationElementKey = MessageBackedKey(id)
val threadHeader: ConversationElementKey = ThreadHeaderKey
}
}
private data class MessageBackedKey(val id: Long) : ConversationElementKey
private object ThreadHeaderKey : ConversationElementKey
/**
* ConversationDataSource for V2. Assumes that ThreadId is never -1L.
@@ -46,11 +49,13 @@ class ConversationDataSource(
private val threadId: Long,
private val messageRequestData: ConversationData.MessageRequestData,
private val showUniversalExpireTimerUpdate: Boolean,
private var baseSize: Int
private var baseSize: Int,
private val messageRequestRepository: MessageRequestRepository = MessageRequestRepository(context)
) : PagedDataSource<ConversationElementKey, ConversationElement> {
companion object {
private val TAG = Log.tag(ConversationDataSource::class.java)
private const val THREAD_HEADER_COUNT = 1
}
init {
@@ -64,6 +69,7 @@ class ConversationDataSource(
override fun size(): Int {
val startTime = System.currentTimeMillis()
val size: Int = getSizeInternal() +
THREAD_HEADER_COUNT +
messageRequestData.includeWarningUpdateMessage().toInt() +
messageRequestData.isHidden.toInt() +
showUniversalExpireTimerUpdate.toInt()
@@ -85,7 +91,7 @@ class ConversationDataSource(
return SignalDatabase.messages.getMessageCountForThread(threadId)
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<ConversationElement> {
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<ConversationElement> {
val stopwatch = Stopwatch("load($start, $length), thread $threadId")
var records: MutableList<MessageRecord> = ArrayList(length)
val mentionHelper = MentionHelper()
@@ -115,11 +121,11 @@ class ConversationDataSource(
}
}
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= size())) {
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= totalSize)) {
records.add(NoGroupsInCommon(threadId, messageRequestData.isGroup))
}
if (messageRequestData.isHidden && (start + length >= size())) {
if (messageRequestData.isHidden && (start + length >= totalSize)) {
records.add(RemovedContactHidden(threadId))
}
@@ -174,12 +180,26 @@ class ConversationDataSource(
}
stopwatch.split("conversion")
val threadHeaderIndex = totalSize - THREAD_HEADER_COUNT
val threadHeaders: List<ConversationElement> = if (start + length > threadHeaderIndex) {
listOf(loadThreadHeader())
} else {
emptyList()
}
stopwatch.split("header")
stopwatch.stop(TAG)
return messages
return if (threadHeaders.isNotEmpty()) messages + threadHeaders else messages
}
override fun load(key: ConversationElementKey): ConversationElement? {
if (key is ThreadHeaderKey) {
return loadThreadHeader()
}
if (key !is MessageBackedKey) {
Log.w(TAG, "Loading non-message related id $key")
return null
@@ -249,10 +269,15 @@ class ConversationDataSource(
override fun getKey(conversationMessage: ConversationElement): ConversationElementKey {
return when (conversationMessage) {
is ConversationMessageElement -> MessageBackedKey(conversationMessage.conversationMessage.messageRecord.id)
is ThreadHeader -> ThreadHeaderKey
else -> throw AssertionError()
}
}
private fun loadThreadHeader(): ThreadHeader {
return ThreadHeader(messageRequestRepository.getRecipientInfo(threadRecipient.id, threadId))
}
private fun ConversationMessage.toMappingModel(): MappingModel<*> {
return if (messageRecord.isUpdate) {
ConversationUpdate(this)

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.conversation.v2.data
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.messagerequests.MessageRequestRecipientInfo
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
sealed interface ConversationMessageElement {
@@ -71,3 +72,13 @@ data class IncomingMedia(
return false
}
}
data class ThreadHeader(val recipientInfo: MessageRequestRecipientInfo) : MappingModel<ThreadHeader> {
override fun areItemsTheSame(newItem: ThreadHeader): Boolean {
return true
}
override fun areContentsTheSame(newItem: ThreadHeader): Boolean {
return false
}
}

View File

@@ -66,7 +66,7 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
}
@Override
public @NonNull List<Conversation> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
public @NonNull List<Conversation> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
SignalTrace.beginSection("ConversationListDataSource#load");
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName() + ", " + conversationFilter);

View File

@@ -149,7 +149,10 @@ class ConversationListViewModel(
conversationListDataSource
.subscribeOn(Schedulers.io())
.firstOrError()
.map { dataSource -> dataSource.load(0, dataSource.size()) { disposables.isDisposed } }
.map { dataSource ->
val totalSize = dataSource.size()
dataSource.load(0, totalSize, totalSize) { disposables.isDisposed }
}
.subscribe { newSelection -> setSelection(newSelection) }
.addTo(disposables)
}

View File

@@ -4,6 +4,7 @@ import android.graphics.Canvas
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.conversation.ConversationAdapter
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
import kotlin.math.min
/**
@@ -28,7 +29,7 @@ class GiphyMp4ItemDecoration(
} else {
val footerViewHolder = parent.children
.map { parent.getChildViewHolder(it) }
.filterIsInstance(ConversationAdapter.FooterViewHolder::class.java)
.filter { it is ConversationAdapter.FooterViewHolder || it is ConversationAdapterV2.ThreadHeaderViewHolder }
.firstOrNull()
if (footerViewHolder == null) {

View File

@@ -66,7 +66,7 @@ final class GiphyMp4PagedDataSource implements PagedDataSource<String, GiphyImag
}
@Override
public @NonNull List<GiphyImage> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
public @NonNull List<GiphyImage> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
try {
Log.d(TAG, "Loading from " + start + " to " + (start + length));
return new LinkedList<>(performFetch(start, length).getData());

View File

@@ -24,7 +24,7 @@ class LogDataSource(
return prefixLines.size + logDatabase.getLogCountBeforeTime(untilTime)
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<LogLine> {
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<LogLine> {
if (start + length < prefixLines.size) {
return prefixLines.subList(start, start + length)
} else if (start < prefixLines.size) {

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messagerequests
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Thread recipient and message request state information necessary to render
* a thread header.
*/
data class MessageRequestRecipientInfo(
val recipient: Recipient,
val groupInfo: GroupInfo = GroupInfo.ZERO,
val sharedGroups: List<String> = emptyList(),
val messageRequestState: MessageRequestState? = null
)

View File

@@ -72,6 +72,31 @@ public final class MessageRequestRepository {
});
}
@WorkerThread
public @NonNull MessageRequestRecipientInfo getRecipientInfo(@NonNull RecipientId recipientId, long threadId) {
List<String> sharedGroups = SignalDatabase.groups().getPushGroupNamesContainingMember(recipientId);
Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroup(recipientId);
GroupInfo groupInfo = GroupInfo.ZERO;
if (groupRecord.isPresent()) {
if (groupRecord.get().isV2Group()) {
DecryptedGroup decryptedGroup = groupRecord.get().requireV2GroupProperties().getDecryptedGroup();
groupInfo = new GroupInfo(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount(), decryptedGroup.getDescription());
} else {
groupInfo = new GroupInfo(groupRecord.get().getMembers().size(), 0, "");
}
}
Recipient recipient = Recipient.resolved(recipientId);
return new MessageRequestRecipientInfo(
recipient,
groupInfo,
sharedGroups,
getMessageRequestState(recipient, threadId)
);
}
@WorkerThread
public @NonNull MessageRequestState getMessageRequestState(@NonNull Recipient recipient, long threadId) {
if (recipient.isBlocked()) {

View File

@@ -15,7 +15,7 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour
return SignalDatabase.messages.getNumberOfStoryReplies(parentStoryId)
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ReplyBody> {
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ReplyBody> {
val results: MutableList<ReplyBody> = ArrayList(length)
SignalDatabase.messages.getStoryReplies(parentStoryId).use { cursor ->
cursor.moveToPosition(start - 1)

View File

@@ -53,7 +53,7 @@ class ContactSearchPagedDataSourceTest {
@Test
fun `Given recentsWHeader and individualsWHeaderWExpand, when I load 12, then I expect properly structured output`() {
val testSubject = createTestSubject()
val result = testSubject.load(0, 12) { false }
val result = testSubject.load(0, 12, 20) { false }
val expected = listOf(
ContactSearchKey.Header(ContactSearchConfiguration.SectionKey.RECENTS),
@@ -78,7 +78,7 @@ class ContactSearchPagedDataSourceTest {
@Test
fun `Given recentsWHeader and individualsWHeaderWExpand, when I load 10 with offset 5, then I expect properly structured output`() {
val testSubject = createTestSubject()
val result = testSubject.load(5, 10) { false }
val result = testSubject.load(5, 10, 15) { false }
val expected = listOf(
ContactSearchKey.RecipientSearchKey(RecipientId.UNKNOWN, false),
@@ -101,7 +101,7 @@ class ContactSearchPagedDataSourceTest {
@Test
fun `Given storiesWithHeaderAndExtras, when I load 11, then I expect properly structured output`() {
val testSubject = createStoriesSubject()
val result = testSubject.load(0, 12) { false }
val result = testSubject.load(0, 12, 12) { false }
val expected = listOf(
ContactSearchKey.Header(ContactSearchConfiguration.SectionKey.STORIES),
@@ -136,7 +136,7 @@ class ContactSearchPagedDataSourceTest {
fun `Given only arbitrary elements, when I load 1, then I expect 1`() {
val testSubject = createArbitrarySubject()
val expected = ContactSearchData.Arbitrary("two", bundleOf("n" to "two"))
val actual = testSubject.load(1, 1) { false }[0] as ContactSearchData.Arbitrary
val actual = testSubject.load(1, 1, 1) { false }[0] as ContactSearchData.Arbitrary
Assert.assertEquals(expected.data?.getString("n"), actual.data?.getString("n"))
}

View File

@@ -6,7 +6,6 @@ import androidx.annotation.Nullable;
import org.signal.paging.PagedDataSource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.UUID;
@@ -25,7 +24,7 @@ class MainDataSource implements PagedDataSource<String, Item> {
}
@Override
public @NonNull List<Item> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
public @NonNull List<Item> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {

View File

@@ -66,6 +66,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
final int loadStart;
final int loadEnd;
final int totalSize;
synchronized (loadState) {
if (loadState.size() == 0) {
@@ -94,7 +95,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
return;
}
int totalSize = loadState.size();
totalSize = loadState.size();
loadState.markRange(loadStart, loadEnd);
@@ -107,7 +108,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
return;
}
List<Data> loaded = dataSource.load(loadStart, loadEnd - loadStart, () -> invalidated);
List<Data> loaded = dataSource.load(loadStart, loadEnd - loadStart, totalSize, () -> invalidated);
if (invalidated) {
Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! Just after data was loaded."));

View File

@@ -19,13 +19,13 @@ public interface PagedDataSource<Key, Data> {
/**
* @param start The index of the first item that should be included in your results.
* @param length The total number of items you should return.
* @param totalSize The total number of items in the data source
* @param cancellationSignal An object that you can check to see if the load operation was canceled.
*
* @return A list of length {@code length} that represents the data starting at {@code start}.
* If you don't have the full range, just populate what you can.
*/
@WorkerThread
@NonNull List<Data> load(int start, int length, @NonNull CancellationSignal cancellationSignal);
@NonNull List<Data> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal);
@WorkerThread
@Nullable Data load(Key key);