Consolidate duplicated logic to retrieve groups in common.

Merges all of these into GroupsInCommonRepository:
- ConversationSettingsRepository.getGroupsInCommon()
- CallLinkIncomingRequestRepository.getGroupsInCommon()
- ContactSearchPagedDataSourceRepository.getGroupsInCommon()
- ReviewUtil.getGroupsInCommonCount()
- AboutSheetRepository.getGroupsInCommonCount()
This commit is contained in:
Jeffrey Starke
2025-04-08 11:02:29 -04:00
committed by Michelle Tang
parent c9795141df
commit aa7b61ecb1
15 changed files with 112 additions and 144 deletions

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.LiveData
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.rx3.asObservable
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.storageservice.protos.groups.local.DecryptedGroup
@@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupProtoUtil
import org.thoughtcrime.securesms.groups.GroupsInCommonRepository
import org.thoughtcrime.securesms.groups.LiveGroup
import org.thoughtcrime.securesms.groups.v2.GroupAddMembersResult
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository
@@ -101,23 +103,8 @@ class ConversationSettingsRepository(
}
fun getGroupsInCommon(recipientId: RecipientId): Observable<List<Recipient>> {
return Recipient.observable(recipientId).flatMapSingle { recipient ->
if (recipient.hasGroupsInCommon) {
Single.fromCallable {
SignalDatabase
.groups
.getPushGroupsContainingMember(recipientId)
.asSequence()
.filter { it.members.contains(Recipient.self().id) }
.map(GroupRecord::recipientId)
.map(Recipient::resolved)
.sortedBy { gr -> gr.getDisplayName(context) }
.toList()
}.observeOn(Schedulers.io())
} else {
Single.just(listOf())
}
}
return GroupsInCommonRepository.getGroupsInCommon(context, recipientId)
.asObservable()
}
fun getGroupMembership(recipientId: RecipientId, consumer: (List<RecipientId>) -> Unit) {

View File

@@ -1,31 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.contacts.paged.GroupsInCommon
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class CallLinkIncomingRequestRepository {
fun getGroupsInCommon(recipientId: RecipientId): Observable<GroupsInCommon> {
return Recipient.observable(recipientId).flatMapSingle { recipient ->
if (recipient.hasGroupsInCommon) {
Single.fromCallable {
val groupsInCommon = SignalDatabase.groups.getGroupsContainingMember(recipient.id, true)
val total = groupsInCommon.size
val names = groupsInCommon.take(2).map { it.title!! }
GroupsInCommon(total, names)
}.observeOn(Schedulers.io())
} else {
Single.just(GroupsInCommon(0, listOf()))
}
}
}
}

View File

@@ -75,7 +75,7 @@ class CallLinkIncomingRequestSheet : ComposeBottomSheetDialogFragment() {
}
private val viewModel by viewModel {
CallLinkIncomingRequestViewModel(recipientId)
CallLinkIncomingRequestViewModel(requireContext(), recipientId)
}
@Composable

View File

@@ -11,15 +11,18 @@ import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import kotlinx.coroutines.rx3.asObservable
import org.thoughtcrime.securesms.groups.GroupsInCommonRepository
import org.thoughtcrime.securesms.groups.GroupsInCommonSummary
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore
class CallLinkIncomingRequestViewModel(
private val context: Context,
private val recipientId: RecipientId
) : ViewModel() {
private val repository = CallLinkIncomingRequestRepository()
private val store = RxStore(CallLinkIncomingRequestState())
private val disposables = CompositeDisposable().apply {
add(store)
@@ -39,10 +42,16 @@ class CallLinkIncomingRequestViewModel(
)
}
disposables += store.update(repository.getGroupsInCommon(recipientId).toFlowable(BackpressureStrategy.LATEST)) { g, s ->
s.copy(groupsInCommon = g.toDisplayText(context))
disposables += store.update(getGroupsInCommon()) { groupsInCommon, state ->
state.copy(groupsInCommon = groupsInCommon.toDisplayText(context))
}
return store.stateFlowable
}
private fun getGroupsInCommon(): Flowable<GroupsInCommonSummary> {
return GroupsInCommonRepository.getGroupsInCommonSummary(context, recipientId)
.asObservable()
.toFlowable(BackpressureStrategy.LATEST)
}
}

View File

@@ -464,7 +464,7 @@ open class ContactSearchAdapter(
override fun bindNumberField(model: RecipientModel) {
val recipient = getRecipient(model)
if (model.knownRecipient.sectionKey == ContactSearchConfiguration.SectionKey.GROUP_MEMBERS) {
number.text = model.knownRecipient.groupsInCommon.toDisplayText(context)
number.text = model.knownRecipient.groupsInCommon.toDisplayText(context, displayGroupsLimit = 2)
number.visible = true
} else if (model.shortSummary && recipient.isGroup) {
val count = recipient.participantIds.size

View File

@@ -6,6 +6,7 @@ import org.thoughtcrime.securesms.contacts.HeaderAction
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.groups.GroupsInCommonSummary
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.search.MessageResult
@@ -32,7 +33,7 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) {
val recipient: Recipient,
val shortSummary: Boolean = false,
val headerLetter: String? = null,
val groupsInCommon: GroupsInCommon = GroupsInCommon(0, listOf())
val groupsInCommon: GroupsInCommonSummary = GroupsInCommonSummary(listOf())
) : ContactSearchData(ContactSearchKey.RecipientSearchKey(recipient.id, false))
/**

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.contacts.paged
import android.database.Cursor
import androidx.annotation.WorkerThread
import org.signal.core.util.requireLong
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.R
@@ -168,6 +169,7 @@ class ContactSearchPagedDataSource(
.filter { contactSearchPagedDataSourceRepository.recipientNameContainsQuery(it.recipient, query) }
}
@WorkerThread
private fun getSectionData(section: ContactSearchConfiguration.Section, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
return when (section) {
is ContactSearchConfiguration.Section.Groups -> getGroupContactsData(section, query, startIndex, endIndex)
@@ -432,6 +434,7 @@ class ContactSearchPagedDataSource(
}
}
@WorkerThread
private fun getGroupMembersContactData(section: ContactSearchConfiguration.Section.GroupMembers, query: String?, startIndex: Int, endIndex: Int): List<ContactSearchData> {
return getGroupMembersSearchIterator(query).use { records ->
readContactData(

View File

@@ -2,6 +2,9 @@ package org.thoughtcrime.securesms.contacts.paged
import android.content.Context
import android.database.Cursor
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.ContactRepository
@@ -13,6 +16,8 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.groups.GroupsInCommonRepository
import org.thoughtcrime.securesms.groups.GroupsInCommonSummary
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.StorySend
import org.thoughtcrime.securesms.recipients.Recipient
@@ -105,14 +110,13 @@ open class ContactSearchPagedDataSourceRepository(
return Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, RecipientTable.ID)))
}
open fun getGroupsInCommon(recipient: Recipient): GroupsInCommon {
val groupsInCommon = SignalDatabase.groups.getPushGroupsContainingMember(recipient.id)
val groupRecipientIds = groupsInCommon.take(2).map { it.recipientId }
val names = Recipient.resolvedList(groupRecipientIds)
.map { it.getDisplayName(context) }
.sorted()
return GroupsInCommon(groupsInCommon.size, names)
@WorkerThread
open fun getGroupsInCommon(recipient: Recipient): GroupsInCommonSummary {
return runBlocking {
GroupsInCommonRepository
.getGroupsInCommonSummary(context, recipient.id)
.first()
}
}
open fun getRecipientFromGroupRecord(groupRecord: GroupRecord): Recipient {

View File

@@ -1,34 +0,0 @@
package org.thoughtcrime.securesms.contacts.paged
import android.content.Context
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
/**
* Groups in common helper class
*/
data class GroupsInCommon(
private val total: Int,
private val names: List<String>
) {
fun toDisplayText(context: Context): String {
return when (total) {
0 -> {
Log.w(TAG, "Member with no groups in common!")
return ""
}
1 -> context.getString(R.string.MessageRequestProfileView_member_of_one_group, names[0])
2 -> context.getString(R.string.MessageRequestProfileView_member_of_two_groups, names[0], names[1])
else -> context.getString(
R.string.MessageRequestProfileView_member_of_many_groups,
names[0],
names[1],
context.resources.getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, total - 2, total - 2)
)
}
}
companion object {
private val TAG = Log.tag(GroupsInCommon::class.java)
}
}

View File

@@ -6,11 +6,17 @@
package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.annotation.Discouraged
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.rx3.asFlow
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.signal.core.util.logging.logV
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -18,22 +24,41 @@ import org.thoughtcrime.securesms.recipients.RecipientId
/**
* Centralizes operations for retrieving groups that a given recipient has in common with another user.
*/
class GroupsInCommonRepository(private val context: Context) {
object GroupsInCommonRepository {
fun getGroupsInCommon(recipientId: RecipientId): Flow<List<Recipient>> {
@WorkerThread
@JvmStatic
@Discouraged("Use getGroupsInCommonCount instead")
fun getGroupsInCommonCountSync(recipientId: RecipientId): Int = runBlocking {
getGroupsInCommonCount(recipientId)
}
suspend fun getGroupsInCommonCount(recipientId: RecipientId): Int = withContext(Dispatchers.IO) {
SignalDatabase.groups
.getPushGroupsContainingMember(recipientId)
.count { it.members.contains(Recipient.self().id) }
}
fun getGroupsInCommonSummary(context: Context, recipientId: RecipientId): Flow<GroupsInCommonSummary> {
return getGroupsInCommon(context, recipientId)
.map(::GroupsInCommonSummary)
}
fun getGroupsInCommon(context: Context, recipientId: RecipientId): Flow<List<Recipient>> {
return Recipient.observable(recipientId)
.asFlow()
.map { recipient ->
if (recipient.hasGroupsInCommon) {
getGroupsContainingRecipient(recipientId)
getGroupsContainingRecipient(context, recipientId)
} else {
emptyList()
}
}
}
private suspend fun getGroupsContainingRecipient(recipientId: RecipientId): List<Recipient> = withContext(Dispatchers.IO) {
SignalDatabase.groups.getPushGroupsContainingMember(recipientId)
private suspend fun getGroupsContainingRecipient(context: Context, recipientId: RecipientId): List<Recipient> = withContext(Dispatchers.IO) {
SignalDatabase.groups
.getPushGroupsContainingMember(recipientId)
.asSequence()
.filter { it.members.contains(Recipient.self().id) }
.map { groupRecord -> Recipient.resolved(groupRecord.recipientId) }
@@ -41,3 +66,35 @@ class GroupsInCommonRepository(private val context: Context) {
.toList()
}
}
/**
* A summary of groups that recipients have in common.
*/
data class GroupsInCommonSummary(
private val groups: List<Recipient>
) {
companion object {
private val TAG = Log.tag(GroupsInCommonSummary::class.java)
}
fun toDisplayText(context: Context, displayGroupsLimit: Int? = null): String {
val displayGroupNames = (displayGroupsLimit?.let(groups::take) ?: groups)
.map { it.getDisplayName(context) }
return when (displayGroupNames.size) {
0 -> "".logV(TAG, "Member with no groups in common!")
1 -> context.getString(R.string.MessageRequestProfileView_member_of_one_group, displayGroupNames[0])
2 -> context.getString(R.string.MessageRequestProfileView_member_of_two_groups, displayGroupNames[0], displayGroupNames[1])
else -> {
val specificGroupNames = displayGroupNames.take(2)
val additionalGroupsCount = displayGroupNames.size - specificGroupNames.size
context.getString(
R.string.MessageRequestProfileView_member_of_many_groups,
specificGroupNames[0],
specificGroupNames[1],
context.resources.getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, additionalGroupsCount, additionalGroupsCount)
)
}
}
}
}

View File

@@ -48,7 +48,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.AvatarImage
import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
import org.thoughtcrime.securesms.groups.GroupsInCommonRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.CommunicationActions
@@ -74,8 +73,8 @@ class GroupsInCommonActivity : PassphraseRequiredActivity() {
private val viewModel by viewModel {
GroupsInCommonViewModel(
recipientId = intent.getParcelableExtraCompat(EXTRA_RECIPIENT_ID, RecipientId::class.java)!!,
groupsInCommonRepo = GroupsInCommonRepository(context = this)
context = this,
recipientId = intent.getParcelableExtraCompat(EXTRA_RECIPIENT_ID, RecipientId::class.java)!!
)
}
@@ -85,10 +84,10 @@ class GroupsInCommonActivity : PassphraseRequiredActivity() {
setContent {
SignalTheme {
GroupsInCommonScreen(
activity = this,
viewModel = viewModel,
onNavigateBack = ::supportFinishAfterTransition,
onNavigateToConversation = ::navigateToConversation,
activity = this
onNavigateToConversation = ::navigateToConversation
)
}
}
@@ -107,8 +106,8 @@ class GroupsInCommonActivity : PassphraseRequiredActivity() {
@Composable
private fun GroupsInCommonScreen(
viewModel: GroupsInCommonViewModel,
activity: Activity,
viewModel: GroupsInCommonViewModel,
onNavigateBack: () -> Unit = {},
onNavigateToConversation: (recipient: Recipient) -> Unit = {}
) {

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.groups.ui.incommon
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
@@ -15,11 +16,11 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class GroupsInCommonViewModel(
recipientId: RecipientId,
groupsInCommonRepo: GroupsInCommonRepository
context: Context,
recipientId: RecipientId
) : ViewModel() {
val groups: StateFlow<List<Recipient>> = groupsInCommonRepo.getGroupsInCommon(recipientId)
val groups: StateFlow<List<Recipient>> = GroupsInCommonRepository.getGroupsInCommon(context, recipientId)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),

View File

@@ -12,12 +12,12 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsInCommonRepository;
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.IOException;
import java.util.List;
@@ -57,7 +57,7 @@ class ReviewCardRepository {
@WorkerThread
int loadGroupsInCommonCount(@NonNull ReviewRecipient reviewRecipient) {
return ReviewUtil.getGroupsInCommonCount(context, reviewRecipient.getRecipient().getId());
return GroupsInCommonRepository.getGroupsInCommonCountSync(reviewRecipient.getRecipient().getId());
}
void block(@NonNull ReviewCard reviewCard, @NonNull Runnable onActionCompleteListener) {

View File

@@ -1,28 +0,0 @@
package org.thoughtcrime.securesms.profiles.spoofing;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
public final class ReviewUtil {
private ReviewUtil() { }
@WorkerThread
public static int getGroupsInCommonCount(@NonNull Context context, @NonNull RecipientId recipientId) {
return Stream.of(SignalDatabase.groups()
.getPushGroupsContainingMember(recipientId))
.filter(g -> g.getMembers().contains(Recipient.self().getId()))
.map(GroupRecord::getRecipientId)
.toList()
.size();
}
}

View File

@@ -7,16 +7,16 @@ package org.thoughtcrime.securesms.recipients.ui.about
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.rx3.rxSingle
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupsInCommonRepository
import org.thoughtcrime.securesms.recipients.RecipientId
class AboutSheetRepository {
fun getGroupsInCommonCount(recipientId: RecipientId): Single<Int> {
return Single.fromCallable {
SignalDatabase.groups.getPushGroupsContainingMember(recipientId).size
}.subscribeOn(Schedulers.io())
return rxSingle { GroupsInCommonRepository.getGroupsInCommonCount(recipientId) }
}
fun getVerified(recipientId: RecipientId): Single<Boolean> {