mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 04:28:35 +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 goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,15 +247,16 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
.flatMapObservable { it.items.data }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onNext = {
|
||||
SignalLocalMetrics.ConversationOpen.onDataPostedToMain()
|
||||
if (firstRender) {
|
||||
SignalLocalMetrics.ConversationOpen.onDataPostedToMain()
|
||||
}
|
||||
|
||||
adapter.submitList(it) {
|
||||
scrollToPositionDelegate.notifyListCommitted()
|
||||
|
||||
binding.conversationItemRecycler.doAfterNextLayout {
|
||||
SignalLocalMetrics.ConversationOpen.onRenderFinished()
|
||||
|
||||
if (firstRender) {
|
||||
if (firstRender) {
|
||||
binding.conversationItemRecycler.doAfterNextLayout {
|
||||
SignalLocalMetrics.ConversationOpen.onRenderFinished()
|
||||
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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
public @NonNull MessageRequestState getMessageRequestState(@NonNull Recipient recipient, long threadId) {
|
||||
if (recipient.isBlocked()) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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."));
|
||||
|
||||
@@ -17,15 +17,15 @@ public interface PagedDataSource<Key, Data> {
|
||||
int size();
|
||||
|
||||
/**
|
||||
* @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 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.
|
||||
* 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);
|
||||
|
||||
Reference in New Issue
Block a user