mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Display thread header in CFv2.
This commit is contained in:
committed by
Greyson Parrelli
parent
ffbbdc1576
commit
3ba128793a
@@ -115,5 +115,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
|||||||
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
|
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
|
||||||
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
|
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
|
||||||
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||||
|
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class CallLogPagedDataSource(
|
|||||||
return callsCount + hasFilter.toInt() + hasCallLinkRow.toInt()
|
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 calls = mutableListOf<CallLogRow>()
|
||||||
val callLimit = length - hasCallLinkRow.toInt()
|
val callLimit = length - hasCallLinkRow.toInt()
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class ContactSearchPagedDataSource(
|
|||||||
return searchSize
|
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) {
|
val sections: List<ContactSearchConfiguration.Section> = if (displayEmptyState) {
|
||||||
contactConfiguration.emptyStateSections
|
contactConfiguration.emptyStateSections
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import org.whispersystems.signalservice.api.push.ServiceId;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,7 +98,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
|
||||||
List<MessageRecord> records = new ArrayList<>(length);
|
List<MessageRecord> records = new ArrayList<>(length);
|
||||||
MentionHelper mentionHelper = new MentionHelper();
|
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()));
|
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));
|
records.add(new InMemoryMessageRecord.RemovedContactHidden(threadId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
|||||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||||
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
|
||||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
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.DonateToSignalFragment;
|
||||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType;
|
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
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.WindowUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
|
||||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||||
|
|
||||||
import java.io.IOException;
|
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
|
@Override
|
||||||
public void onActivatePaymentsClicked() {
|
public void onActivatePaymentsClicked() {
|
||||||
Intent intent = new Intent(requireContext(), PaymentsActivity.class);
|
Intent intent = new Intent(requireContext(), PaymentsActivity.class);
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ public class ConversationIntents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static long resolveThreadId(@NonNull RecipientId recipientId, long threadId) {
|
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...");
|
Log.w(TAG, "Getting thread id from database...");
|
||||||
// TODO [alex] -- Yes, this hits the database. No, we shouldn't be doing this.
|
// TODO [alex] -- Yes, this hits the database. No, we shouldn't be doing this.
|
||||||
return SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(recipientId));
|
return SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.resolved(recipientId));
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import org.signal.core.util.logging.Log
|
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.R
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
|
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
|
||||||
|
import org.thoughtcrime.securesms.conversation.ConversationBannerView
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable
|
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.IncomingTextOnly
|
||||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingMedia
|
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingMedia
|
||||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
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.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable
|
||||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer
|
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.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.CachedInflater
|
||||||
|
import org.thoughtcrime.securesms.util.HtmlUtil
|
||||||
import org.thoughtcrime.securesms.util.Projection
|
import org.thoughtcrime.securesms.util.Projection
|
||||||
import org.thoughtcrime.securesms.util.ProjectionList
|
import org.thoughtcrime.securesms.util.ProjectionList
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||||
@@ -65,6 +74,8 @@ class ConversationAdapterV2(
|
|||||||
private val condensedMode: ConversationItemDisplayMode? = null
|
private val condensedMode: ConversationItemDisplayMode? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
registerFactory(ThreadHeader::class.java, ::ThreadHeaderViewHolder, R.layout.conversation_item_banner)
|
||||||
|
|
||||||
registerFactory(ConversationUpdate::class.java) { parent ->
|
registerFactory(ConversationUpdate::class.java) { parent ->
|
||||||
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_update, parent, false)
|
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_update, parent, false)
|
||||||
ConversationUpdateViewHolder(view)
|
ConversationUpdateViewHolder(view)
|
||||||
@@ -91,14 +102,14 @@ class ConversationAdapterV2(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAdapterPositionForMessagePosition(startPosition: Int): Int {
|
/** [messagePosition] is one-based index and adapter is zero-based. */
|
||||||
return startPosition - 1
|
fun getAdapterPositionForMessagePosition(messagePosition: Int): Int {
|
||||||
|
return messagePosition - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastVisibleConversationMessage(position: Int): ConversationMessage? {
|
fun getLastVisibleConversationMessage(position: Int): ConversationMessage? {
|
||||||
return try {
|
return try {
|
||||||
// todo [cody] handle conversation banner adjustment
|
getConversationMessage(position) ?: getConversationMessage(position - 1)
|
||||||
getConversationMessage(position)
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
} catch (e: IndexOutOfBoundsException) {
|
||||||
Log.w(TAG, "Race condition changed size of conversation", e)
|
Log.w(TAG, "Race condition changed size of conversation", e)
|
||||||
null
|
null
|
||||||
@@ -131,6 +142,7 @@ class ConversationAdapterV2(
|
|||||||
override fun getConversationMessage(position: Int): ConversationMessage? {
|
override fun getConversationMessage(position: Int): ConversationMessage? {
|
||||||
return when (val item = getItem(position)) {
|
return when (val item = getItem(position)) {
|
||||||
is ConversationMessageElement -> item.conversationMessage
|
is ConversationMessageElement -> item.conversationMessage
|
||||||
|
is ThreadHeader -> null
|
||||||
null -> null
|
null -> null
|
||||||
else -> throw AssertionError("Invalid item: ${item.javaClass}")
|
else -> throw AssertionError("Invalid item: ${item.javaClass}")
|
||||||
}
|
}
|
||||||
@@ -326,4 +338,75 @@ class ConversationAdapterV2(
|
|||||||
return bindable.getColorizerProjections(coordinateRoot)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,15 +247,16 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
.flatMapObservable { it.items.data }
|
.flatMapObservable { it.items.data }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeBy(onNext = {
|
.subscribeBy(onNext = {
|
||||||
|
if (firstRender) {
|
||||||
SignalLocalMetrics.ConversationOpen.onDataPostedToMain()
|
SignalLocalMetrics.ConversationOpen.onDataPostedToMain()
|
||||||
|
}
|
||||||
|
|
||||||
adapter.submitList(it) {
|
adapter.submitList(it) {
|
||||||
scrollToPositionDelegate.notifyListCommitted()
|
scrollToPositionDelegate.notifyListCommitted()
|
||||||
|
|
||||||
|
if (firstRender) {
|
||||||
binding.conversationItemRecycler.doAfterNextLayout {
|
binding.conversationItemRecycler.doAfterNextLayout {
|
||||||
SignalLocalMetrics.ConversationOpen.onRenderFinished()
|
SignalLocalMetrics.ConversationOpen.onRenderFinished()
|
||||||
|
|
||||||
if (firstRender) {
|
|
||||||
firstRender = false
|
firstRender = false
|
||||||
doAfterFirstRender()
|
doAfterFirstRender()
|
||||||
animationsAllowed = true
|
animationsAllowed = true
|
||||||
@@ -891,6 +892,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
|
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
|
||||||
// TODO [alex] -- ("Not yet implemented")
|
// 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 {
|
private inner class ConversationOptionsMenuCallback : ConversationOptionsMenu.Callback {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class ConversationRecipientRepository(threadId: Long) {
|
|||||||
.flatMapObservable { Recipient.observable(it) }
|
.flatMapObservable { Recipient.observable(it) }
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
|
.distinctUntilChanged { previous, next -> previous === next || previous.hasSameContent(next) }
|
||||||
.replay(1)
|
.replay(1)
|
||||||
.refCount()
|
.refCount()
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -29,9 +34,12 @@ class ConversationRepository(context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun getConversationThreadState(threadId: Long, requestedStartPosition: Int): Single<ConversationThreadState> {
|
fun getConversationThreadState(threadId: Long, requestedStartPosition: Int): Single<ConversationThreadState> {
|
||||||
return Single.fromCallable {
|
return Single.fromCallable {
|
||||||
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted()
|
|
||||||
val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)!!
|
val recipient = SignalDatabase.threads.getRecipientForThreadId(threadId)!!
|
||||||
|
|
||||||
|
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted()
|
||||||
val metadata = oldConversationRepository.getConversationData(threadId, recipient, requestedStartPosition)
|
val metadata = oldConversationRepository.getConversationData(threadId, recipient, requestedStartPosition)
|
||||||
|
SignalLocalMetrics.ConversationOpen.onMetadataLoaded()
|
||||||
|
|
||||||
val messageRequestData = metadata.messageRequestData
|
val messageRequestData = metadata.messageRequestData
|
||||||
val dataSource = ConversationDataSource(
|
val dataSource = ConversationDataSource(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
@@ -48,9 +56,7 @@ class ConversationRepository(context: Context) {
|
|||||||
ConversationThreadState(
|
ConversationThreadState(
|
||||||
items = PagedData.createForObservable(dataSource, config),
|
items = PagedData.createForObservable(dataSource, config),
|
||||||
meta = metadata
|
meta = metadata
|
||||||
).apply {
|
)
|
||||||
SignalLocalMetrics.ConversationOpen.onMetadataLoaded()
|
|
||||||
}
|
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ class ConversationViewModel(
|
|||||||
_recipient.onNext(it)
|
_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)
|
disposables += repository.getConversationThreadState(threadId, requestedStartingPosition)
|
||||||
.subscribeBy(onSuccess = {
|
.subscribeBy(onSuccess = {
|
||||||
pagingController.set(it.items.controller)
|
pagingController.set(it.items.controller)
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import io.reactivex.rxjava3.subjects.PublishSubject
|
|||||||
import org.signal.core.util.concurrent.subscribeWithSubject
|
import org.signal.core.util.concurrent.subscribeWithSubject
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||||
import org.thoughtcrime.securesms.messagerequests.GroupInfo
|
import org.thoughtcrime.securesms.messagerequests.GroupInfo
|
||||||
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestRecipientInfo
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.MessageData
|
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.RequestReviewDisplayState
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.Status
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.Status
|
||||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
|
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
|
||||||
@@ -70,12 +70,12 @@ class MessageRequestViewModel(
|
|||||||
}
|
}
|
||||||
}.subscribeWithSubject(BehaviorSubject.create(), disposables)
|
}.subscribeWithSubject(BehaviorSubject.create(), disposables)
|
||||||
|
|
||||||
val recipientInfo: Observable<RecipientInfo> = Observable.combineLatest(
|
val recipientInfo: Observable<MessageRequestRecipientInfo> = Observable.combineLatest(
|
||||||
recipientRepository.conversationRecipient,
|
recipientRepository.conversationRecipient,
|
||||||
groupInfo,
|
groupInfo,
|
||||||
groups,
|
groups,
|
||||||
messageDataSubject.map { it.messageState },
|
messageDataSubject.map { it.messageState },
|
||||||
::RecipientInfo
|
::MessageRequestRecipientInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageId
|
import org.thoughtcrime.securesms.database.model.MessageId
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
@@ -33,10 +34,12 @@ private typealias ConversationElement = MappingModel<*>
|
|||||||
sealed interface ConversationElementKey {
|
sealed interface ConversationElementKey {
|
||||||
companion object {
|
companion object {
|
||||||
fun forMessage(id: Long): ConversationElementKey = MessageBackedKey(id)
|
fun forMessage(id: Long): ConversationElementKey = MessageBackedKey(id)
|
||||||
|
val threadHeader: ConversationElementKey = ThreadHeaderKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class MessageBackedKey(val id: Long) : ConversationElementKey
|
private data class MessageBackedKey(val id: Long) : ConversationElementKey
|
||||||
|
private object ThreadHeaderKey : ConversationElementKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConversationDataSource for V2. Assumes that ThreadId is never -1L.
|
* ConversationDataSource for V2. Assumes that ThreadId is never -1L.
|
||||||
@@ -46,11 +49,13 @@ class ConversationDataSource(
|
|||||||
private val threadId: Long,
|
private val threadId: Long,
|
||||||
private val messageRequestData: ConversationData.MessageRequestData,
|
private val messageRequestData: ConversationData.MessageRequestData,
|
||||||
private val showUniversalExpireTimerUpdate: Boolean,
|
private val showUniversalExpireTimerUpdate: Boolean,
|
||||||
private var baseSize: Int
|
private var baseSize: Int,
|
||||||
|
private val messageRequestRepository: MessageRequestRepository = MessageRequestRepository(context)
|
||||||
) : PagedDataSource<ConversationElementKey, ConversationElement> {
|
) : PagedDataSource<ConversationElementKey, ConversationElement> {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(ConversationDataSource::class.java)
|
private val TAG = Log.tag(ConversationDataSource::class.java)
|
||||||
|
private const val THREAD_HEADER_COUNT = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -64,6 +69,7 @@ class ConversationDataSource(
|
|||||||
override fun size(): Int {
|
override fun size(): Int {
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
val size: Int = getSizeInternal() +
|
val size: Int = getSizeInternal() +
|
||||||
|
THREAD_HEADER_COUNT +
|
||||||
messageRequestData.includeWarningUpdateMessage().toInt() +
|
messageRequestData.includeWarningUpdateMessage().toInt() +
|
||||||
messageRequestData.isHidden.toInt() +
|
messageRequestData.isHidden.toInt() +
|
||||||
showUniversalExpireTimerUpdate.toInt()
|
showUniversalExpireTimerUpdate.toInt()
|
||||||
@@ -85,7 +91,7 @@ class ConversationDataSource(
|
|||||||
return SignalDatabase.messages.getMessageCountForThread(threadId)
|
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")
|
val stopwatch = Stopwatch("load($start, $length), thread $threadId")
|
||||||
var records: MutableList<MessageRecord> = ArrayList(length)
|
var records: MutableList<MessageRecord> = ArrayList(length)
|
||||||
val mentionHelper = MentionHelper()
|
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))
|
records.add(NoGroupsInCommon(threadId, messageRequestData.isGroup))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageRequestData.isHidden && (start + length >= size())) {
|
if (messageRequestData.isHidden && (start + length >= totalSize)) {
|
||||||
records.add(RemovedContactHidden(threadId))
|
records.add(RemovedContactHidden(threadId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,12 +180,26 @@ class ConversationDataSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopwatch.split("conversion")
|
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)
|
stopwatch.stop(TAG)
|
||||||
|
|
||||||
return messages
|
return if (threadHeaders.isNotEmpty()) messages + threadHeaders else messages
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun load(key: ConversationElementKey): ConversationElement? {
|
override fun load(key: ConversationElementKey): ConversationElement? {
|
||||||
|
if (key is ThreadHeaderKey) {
|
||||||
|
return loadThreadHeader()
|
||||||
|
}
|
||||||
|
|
||||||
if (key !is MessageBackedKey) {
|
if (key !is MessageBackedKey) {
|
||||||
Log.w(TAG, "Loading non-message related id $key")
|
Log.w(TAG, "Loading non-message related id $key")
|
||||||
return null
|
return null
|
||||||
@@ -249,10 +269,15 @@ class ConversationDataSource(
|
|||||||
override fun getKey(conversationMessage: ConversationElement): ConversationElementKey {
|
override fun getKey(conversationMessage: ConversationElement): ConversationElementKey {
|
||||||
return when (conversationMessage) {
|
return when (conversationMessage) {
|
||||||
is ConversationMessageElement -> MessageBackedKey(conversationMessage.conversationMessage.messageRecord.id)
|
is ConversationMessageElement -> MessageBackedKey(conversationMessage.conversationMessage.messageRecord.id)
|
||||||
|
is ThreadHeader -> ThreadHeaderKey
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadThreadHeader(): ThreadHeader {
|
||||||
|
return ThreadHeader(messageRequestRepository.getRecipientInfo(threadRecipient.id, threadId))
|
||||||
|
}
|
||||||
|
|
||||||
private fun ConversationMessage.toMappingModel(): MappingModel<*> {
|
private fun ConversationMessage.toMappingModel(): MappingModel<*> {
|
||||||
return if (messageRecord.isUpdate) {
|
return if (messageRecord.isUpdate) {
|
||||||
ConversationUpdate(this)
|
ConversationUpdate(this)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2.data
|
package org.thoughtcrime.securesms.conversation.v2.data
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||||
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestRecipientInfo
|
||||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
|
|
||||||
sealed interface ConversationMessageElement {
|
sealed interface ConversationMessageElement {
|
||||||
@@ -71,3 +72,13 @@ data class IncomingMedia(
|
|||||||
return false
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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");
|
SignalTrace.beginSection("ConversationListDataSource#load");
|
||||||
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName() + ", " + conversationFilter);
|
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName() + ", " + conversationFilter);
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,10 @@ class ConversationListViewModel(
|
|||||||
conversationListDataSource
|
conversationListDataSource
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.firstOrError()
|
.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) }
|
.subscribe { newSelection -> setSelection(newSelection) }
|
||||||
.addTo(disposables)
|
.addTo(disposables)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.graphics.Canvas
|
|||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +29,7 @@ class GiphyMp4ItemDecoration(
|
|||||||
} else {
|
} else {
|
||||||
val footerViewHolder = parent.children
|
val footerViewHolder = parent.children
|
||||||
.map { parent.getChildViewHolder(it) }
|
.map { parent.getChildViewHolder(it) }
|
||||||
.filterIsInstance(ConversationAdapter.FooterViewHolder::class.java)
|
.filter { it is ConversationAdapter.FooterViewHolder || it is ConversationAdapterV2.ThreadHeaderViewHolder }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
|
||||||
if (footerViewHolder == null) {
|
if (footerViewHolder == null) {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ final class GiphyMp4PagedDataSource implements PagedDataSource<String, GiphyImag
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
try {
|
||||||
Log.d(TAG, "Loading from " + start + " to " + (start + length));
|
Log.d(TAG, "Loading from " + start + " to " + (start + length));
|
||||||
return new LinkedList<>(performFetch(start, length).getData());
|
return new LinkedList<>(performFetch(start, length).getData());
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class LogDataSource(
|
|||||||
return prefixLines.size + logDatabase.getLogCountBeforeTime(untilTime)
|
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) {
|
if (start + length < prefixLines.size) {
|
||||||
return prefixLines.subList(start, start + length)
|
return prefixLines.subList(start, start + length)
|
||||||
} else if (start < prefixLines.size) {
|
} else if (start < prefixLines.size) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
@WorkerThread
|
||||||
public @NonNull MessageRequestState getMessageRequestState(@NonNull Recipient recipient, long threadId) {
|
public @NonNull MessageRequestState getMessageRequestState(@NonNull Recipient recipient, long threadId) {
|
||||||
if (recipient.isBlocked()) {
|
if (recipient.isBlocked()) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour
|
|||||||
return SignalDatabase.messages.getNumberOfStoryReplies(parentStoryId)
|
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)
|
val results: MutableList<ReplyBody> = ArrayList(length)
|
||||||
SignalDatabase.messages.getStoryReplies(parentStoryId).use { cursor ->
|
SignalDatabase.messages.getStoryReplies(parentStoryId).use { cursor ->
|
||||||
cursor.moveToPosition(start - 1)
|
cursor.moveToPosition(start - 1)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class ContactSearchPagedDataSourceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Given recentsWHeader and individualsWHeaderWExpand, when I load 12, then I expect properly structured output`() {
|
fun `Given recentsWHeader and individualsWHeaderWExpand, when I load 12, then I expect properly structured output`() {
|
||||||
val testSubject = createTestSubject()
|
val testSubject = createTestSubject()
|
||||||
val result = testSubject.load(0, 12) { false }
|
val result = testSubject.load(0, 12, 20) { false }
|
||||||
|
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
ContactSearchKey.Header(ContactSearchConfiguration.SectionKey.RECENTS),
|
ContactSearchKey.Header(ContactSearchConfiguration.SectionKey.RECENTS),
|
||||||
@@ -78,7 +78,7 @@ class ContactSearchPagedDataSourceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Given recentsWHeader and individualsWHeaderWExpand, when I load 10 with offset 5, then I expect properly structured output`() {
|
fun `Given recentsWHeader and individualsWHeaderWExpand, when I load 10 with offset 5, then I expect properly structured output`() {
|
||||||
val testSubject = createTestSubject()
|
val testSubject = createTestSubject()
|
||||||
val result = testSubject.load(5, 10) { false }
|
val result = testSubject.load(5, 10, 15) { false }
|
||||||
|
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
ContactSearchKey.RecipientSearchKey(RecipientId.UNKNOWN, false),
|
ContactSearchKey.RecipientSearchKey(RecipientId.UNKNOWN, false),
|
||||||
@@ -101,7 +101,7 @@ class ContactSearchPagedDataSourceTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Given storiesWithHeaderAndExtras, when I load 11, then I expect properly structured output`() {
|
fun `Given storiesWithHeaderAndExtras, when I load 11, then I expect properly structured output`() {
|
||||||
val testSubject = createStoriesSubject()
|
val testSubject = createStoriesSubject()
|
||||||
val result = testSubject.load(0, 12) { false }
|
val result = testSubject.load(0, 12, 12) { false }
|
||||||
|
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
ContactSearchKey.Header(ContactSearchConfiguration.SectionKey.STORIES),
|
ContactSearchKey.Header(ContactSearchConfiguration.SectionKey.STORIES),
|
||||||
@@ -136,7 +136,7 @@ class ContactSearchPagedDataSourceTest {
|
|||||||
fun `Given only arbitrary elements, when I load 1, then I expect 1`() {
|
fun `Given only arbitrary elements, when I load 1, then I expect 1`() {
|
||||||
val testSubject = createArbitrarySubject()
|
val testSubject = createArbitrarySubject()
|
||||||
val expected = ContactSearchData.Arbitrary("two", bundleOf("n" to "two"))
|
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"))
|
Assert.assertEquals(expected.data?.getString("n"), actual.data?.getString("n"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.annotation.Nullable;
|
|||||||
import org.signal.paging.PagedDataSource;
|
import org.signal.paging.PagedDataSource;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
import java.util.ListIterator;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -25,7 +24,7 @@ class MainDataSource implements PagedDataSource<String, Item> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
try {
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||||||
|
|
||||||
final int loadStart;
|
final int loadStart;
|
||||||
final int loadEnd;
|
final int loadEnd;
|
||||||
|
final int totalSize;
|
||||||
|
|
||||||
synchronized (loadState) {
|
synchronized (loadState) {
|
||||||
if (loadState.size() == 0) {
|
if (loadState.size() == 0) {
|
||||||
@@ -94,7 +95,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int totalSize = loadState.size();
|
totalSize = loadState.size();
|
||||||
|
|
||||||
loadState.markRange(loadStart, loadEnd);
|
loadState.markRange(loadStart, loadEnd);
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Data> loaded = dataSource.load(loadStart, loadEnd - loadStart, () -> invalidated);
|
List<Data> loaded = dataSource.load(loadStart, loadEnd - loadStart, totalSize, () -> invalidated);
|
||||||
|
|
||||||
if (invalidated) {
|
if (invalidated) {
|
||||||
Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! Just after data was loaded."));
|
Log.w(TAG, buildDataNeededLog(aroundIndex, "Invalidated! Just after data was loaded."));
|
||||||
|
|||||||
@@ -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 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 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.
|
* @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}.
|
* @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.
|
* If you don't have the full range, just populate what you can.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@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
|
@WorkerThread
|
||||||
@Nullable Data load(Key key);
|
@Nullable Data load(Key key);
|
||||||
|
|||||||
Reference in New Issue
Block a user