Improvement

This commit is contained in:
SessionHero01 2024-10-21 10:59:31 +11:00
parent b8e98ec9ed
commit 4536aca327
No known key found for this signature in database
11 changed files with 171 additions and 135 deletions

View File

@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -19,6 +21,7 @@ import javax.inject.Inject
@HiltViewModel
class CreateGroupViewModel @Inject constructor(
configFactory: ConfigFactory,
@ApplicationContext appContext: Context,
private val storage: StorageProtocol,
private val groupManagerV2: GroupManagerV2,
): ViewModel() {
@ -28,6 +31,7 @@ class CreateGroupViewModel @Inject constructor(
configFactory = configFactory,
excludingAccountIDs = emptySet(),
scope = viewModelScope,
appContext = appContext,
)
// Input: group name

View File

@ -1,31 +0,0 @@
package org.thoughtcrime.securesms.groups
import androidx.lifecycle.ViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.contacts.Contact
@HiltViewModel(assistedFactory = EditGroupInviteViewModel.Factory::class)
class EditGroupInviteViewModel @AssistedInject constructor(
@Assisted private val groupSessionId: String,
private val storage: StorageProtocol
): ViewModel() {
@AssistedFactory
interface Factory {
fun create(groupSessionId: String): EditGroupInviteViewModel
}
}
data class EditGroupInviteState(
val viewState: EditGroupInviteViewState,
)
data class EditGroupInviteViewState(
val currentMembers: List<GroupMemberState>,
val allContacts: Set<Contact>
)

View File

@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.GroupDisplayInfo
import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.database.StorageProtocol
@ -67,9 +66,9 @@ class EditGroupViewModel @AssistedInject constructor(
memberPendingState
) { _, pending ->
withContext(Dispatchers.Default) {
val currentUserId = checkNotNull(storage.getUserPublicKey()) {
val currentUserId = AccountId(checkNotNull(storage.getUserPublicKey()) {
"User public key is null"
}
})
val displayInfo = storage.getClosedGroupDisplayInfo(groupId.hexString)
?: return@withContext null
@ -118,21 +117,21 @@ class EditGroupViewModel @AssistedInject constructor(
val error: StateFlow<String?> get() = mutableError
// Output:
val excludingAccountIDsFromContactSelection: Set<String>
val excludingAccountIDsFromContactSelection: Set<AccountId>
get() = groupInfo.value?.second?.mapTo(hashSetOf()) { it.accountId }.orEmpty()
private fun createGroupMember(
member: GroupMember,
myAccountId: String,
myAccountId: AccountId,
amIAdmin: Boolean,
pendingState: MemberPendingState?
): GroupMemberState {
var status = ""
var highlightStatus = false
var name = member.name.orEmpty()
var name = member.name.orEmpty().ifEmpty { member.sessionId }
when {
member.sessionId == myAccountId -> {
member.sessionId == myAccountId.hexString -> {
name = context.getString(R.string.you)
}
@ -164,81 +163,81 @@ class EditGroupViewModel @AssistedInject constructor(
}
return GroupMemberState(
accountId = member.sessionId,
accountId = AccountId(member.sessionId),
name = name,
canRemove = amIAdmin && member.sessionId != myAccountId && !member.isAdminOrBeingPromoted,
canPromote = amIAdmin && member.sessionId != myAccountId && !member.isAdminOrBeingPromoted,
canResendPromotion = amIAdmin && member.sessionId != myAccountId && member.promotionFailed,
canResendInvite = amIAdmin && member.sessionId != myAccountId &&
canRemove = amIAdmin && member.sessionId != myAccountId.hexString && !member.isAdminOrBeingPromoted,
canPromote = amIAdmin && member.sessionId != myAccountId.hexString && !member.isAdminOrBeingPromoted,
canResendPromotion = amIAdmin && member.sessionId != myAccountId.hexString && member.promotionFailed,
canResendInvite = amIAdmin && member.sessionId != myAccountId.hexString &&
(member.inviteFailed || member.invitePending),
status = status,
highlightStatus = highlightStatus
)
}
private fun sortMembers(members: MutableList<GroupMember>, currentUserId: String) {
private fun sortMembers(members: MutableList<GroupMember>, currentUserId: AccountId) {
members.sortWith(
compareBy(
{ !it.inviteFailed }, // Failed invite comes first (as false value is less than true)
{ memberPendingState.value[AccountId(it.sessionId)] != MemberPendingState.Inviting }, // "Sending invite" comes first
{ !it.invitePending }, // "Invite sent" comes first
{ !it.isAdminOrBeingPromoted }, // Admins come first
{ it.sessionId != currentUserId }, // Being myself comes first
{ it.sessionId != currentUserId.hexString }, // Being myself comes first
{ it.name }, // Sort by name
{ it.sessionId } // Last resort: sort by account ID
)
)
}
fun onContactSelected(contacts: Set<Contact>) {
fun onContactSelected(contacts: Set<AccountId>) {
performGroupOperation {
try {
// Mark the contacts as pending
memberPendingState.update { states ->
states + contacts.associate { AccountId(it.id) to MemberPendingState.Inviting }
states + contacts.associateWith { MemberPendingState.Inviting }
}
groupManager.inviteMembers(
groupId,
contacts.map { AccountId(it.id) },
contacts.toList(),
shareHistory = false
)
} finally {
// Remove pending state (so the real state will be revealed)
memberPendingState.update { states -> states - contacts.mapTo(hashSetOf()) { AccountId(it.id) } }
memberPendingState.update { states -> states - contacts }
}
}
}
fun onResendInviteClicked(contactSessionId: String) {
onContactSelected(setOf(Contact(contactSessionId)))
fun onResendInviteClicked(contactSessionId: AccountId) {
onContactSelected(setOf(contactSessionId))
}
fun onPromoteContact(memberSessionId: String) {
fun onPromoteContact(memberSessionId: AccountId) {
performGroupOperation {
try {
memberPendingState.update { states ->
states + (AccountId(memberSessionId) to MemberPendingState.Promoting)
states + (memberSessionId to MemberPendingState.Promoting)
}
groupManager.promoteMember(groupId, listOf(AccountId(memberSessionId)))
groupManager.promoteMember(groupId, listOf(memberSessionId))
} finally {
memberPendingState.update { states -> states - AccountId(memberSessionId) }
memberPendingState.update { states -> states - memberSessionId }
}
}
}
fun onRemoveContact(contactSessionId: String, removeMessages: Boolean) {
fun onRemoveContact(contactSessionId: AccountId, removeMessages: Boolean) {
performGroupOperation {
groupManager.removeMembers(
groupAccountId = groupId,
removedMembers = listOf(AccountId(contactSessionId)),
removedMembers = listOf(contactSessionId),
removeMessages = removeMessages
)
}
}
fun onResendPromotionClicked(memberSessionId: String) {
fun onResendPromotionClicked(memberSessionId: AccountId) {
onPromoteContact(memberSessionId)
}
@ -315,7 +314,7 @@ private enum class MemberPendingState {
}
data class GroupMemberState(
val accountId: String,
val accountId: AccountId,
val name: String,
val status: String,
val highlightStatus: Boolean,

View File

@ -6,9 +6,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.GroupMember
@ -24,6 +26,7 @@ import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.messaging.messages.visible.Profile
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPoller
import org.session.libsession.messaging.utilities.MessageAuthentication.buildDeleteMemberContentSignature
import org.session.libsession.messaging.utilities.MessageAuthentication.buildInfoChangeVerifier
import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature
@ -88,7 +91,7 @@ class GroupManagerV2Impl @Inject constructor(
override suspend fun createGroup(
groupName: String,
groupDescription: String,
members: Set<Contact>
members: Set<AccountId>
): Recipient = withContext(dispatcher) {
val ourAccountId =
requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" }
@ -103,9 +106,13 @@ class GroupManagerV2Impl @Inject constructor(
.also(configs.userGroups::set)
}
checkNotNull(group.adminKey) { "Admin key is null for new group creation." }
val adminKey = checkNotNull(group.adminKey) { "Admin key is null for new group creation." }
val groupId = group.groupAccountId
val memberAsRecipients = members.map {
Recipient.from(application, Address.fromSerialized(it.hexString), false)
}
try {
configFactory.withMutableGroupConfigs(groupId) { configs ->
// Update group's information
@ -113,12 +120,14 @@ class GroupManagerV2Impl @Inject constructor(
configs.groupInfo.setDescription(groupDescription)
// Add members
for (member in members) {
for (member in memberAsRecipients) {
configs.groupMembers.set(
GroupMember(
sessionId = member.id,
sessionId = member.address.serialize(),
name = member.name,
profilePicture = member.profilePicture ?: UserPic.DEFAULT,
profilePicture = member.profileAvatar?.let { url ->
member.profileKey?.let { key -> UserPic(url, key) }
} ?: UserPic.DEFAULT,
inviteStatus = INVITE_STATUS_SENT
)
)
@ -165,10 +174,13 @@ class GroupManagerV2Impl @Inject constructor(
JobQueue.shared.add(
InviteContactsJob(
groupSessionId = groupId.hexString,
memberSessionIds = members.map { it.id }.toTypedArray()
memberSessionIds = members.map { it.hexString }.toTypedArray()
)
)
// Also send a group update message
sendGroupUpdateForAddingMembers(groupId, adminKey, members, insertLocally = false)
recipient
} catch (e: Exception) {
Log.e(TAG, "Failed to create group", e)
@ -223,7 +235,7 @@ class GroupManagerV2Impl @Inject constructor(
configs.groupMembers.set(toSet)
}
// Depends on whether we want to share history, we may need to rekey or just adding supplement keys
// Depends on whether we want to share history, we may need to rekey or just adding rsupplement keys
if (shareHistory) {
val memberKey = configs.groupKeys.supplementFor(newMembers.map { it.hexString })
batchRequests.add(
@ -267,7 +279,19 @@ class GroupManagerV2Impl @Inject constructor(
)
)
// Send a member change message to the group
// Send a group update message to the group telling members someone has been invited
sendGroupUpdateForAddingMembers(group, adminKey, newMembers, insertLocally = true)
}
/**
* Send a group update message to the group telling members someone has been invited.
*/
private fun sendGroupUpdateForAddingMembers(
group: AccountId,
adminKey: ByteArray,
newMembers: Collection<AccountId>,
insertLocally: Boolean
) {
val timestamp = clock.currentTimeMills()
val signature = SodiumUtilities.sign(
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.ADDED, timestamp),
@ -284,8 +308,11 @@ class GroupManagerV2Impl @Inject constructor(
)
.build()
).apply { this.sentTimestamp = timestamp }
MessageSender.send(updatedMessage, Destination.ClosedGroup(group.hexString), false).await()
storage.insertGroupInfoChange(updatedMessage, group)
MessageSender.send(updatedMessage, Destination.ClosedGroup(group.hexString), false)
if (insertLocally) {
storage.insertGroupInfoChange(updatedMessage, group)
}
}
override suspend fun removeMembers(
@ -529,16 +556,15 @@ class GroupManagerV2Impl @Inject constructor(
lokiDatabase.deleteGroupInviteReferrer(threadId)
if (approved) {
approveGroupInvite(group, threadId)
approveGroupInvite(group)
} else {
configFactory.withMutableUserConfigs { it.userGroups.eraseClosedGroup(groupId.hexString) }
storage.deleteConversation(threadId)
}
}
private fun approveGroupInvite(
private suspend fun approveGroupInvite(
group: GroupInfo.ClosedGroupInfo,
threadId: Long,
) {
val key = requireNotNull(storage.getUserPublicKey()) {
"Our account ID is not available"
@ -549,6 +575,15 @@ class GroupManagerV2Impl @Inject constructor(
configs.userGroups.set(group.copy(invited = false))
}
val poller = checkNotNull(pollerFactory.pollerFor(group.groupAccountId)) { "Unable to start a poller for groups " }
poller.start()
// We need to wait until we have the first data polled from the poller, otherwise
// we won't have the necessary configs to send invite response/or do anything else
poller.state.filterIsInstance<ClosedGroupPoller.StartedState>()
.filter { it.hadAtLeastOneSuccessfulPoll }
.first()
if (group.adminKey == null) {
// Send an invite response to the group if we are invited as a regular member
val inviteResponse = GroupUpdateInviteResponseMessage.newBuilder()
@ -572,8 +607,6 @@ class GroupManagerV2Impl @Inject constructor(
Unit
}
}
pollerFactory.pollerFor(group.groupAccountId)?.start()
}
override suspend fun handleInvitation(
@ -656,7 +689,7 @@ class GroupManagerV2Impl @Inject constructor(
* @param inviter the invite message sender
* @return The newly created group info if the invitation is processed, null otherwise.
*/
private fun handleInvitation(
private suspend fun handleInvitation(
groupId: AccountId,
groupName: String,
authDataOrAdminKey: ByteArray,
@ -691,8 +724,9 @@ class GroupManagerV2Impl @Inject constructor(
val groupThreadId = storage.getOrCreateThreadIdFor(recipient.address)
storage.setRecipientApprovedMe(recipient, true)
storage.setRecipientApproved(recipient, shouldAutoApprove)
if (shouldAutoApprove) {
approveGroupInvite(closedGroupInfo, groupThreadId)
approveGroupInvite(closedGroupInfo)
} else {
lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString)
storage.insertGroupInviteControlMessage(

View File

@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
@ -21,24 +23,28 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import network.loki.messenger.libsession_util.util.Contact
import org.session.libsession.database.StorageProtocol
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.home.search.getSearchName
@OptIn(FlowPreview::class)
@HiltViewModel(assistedFactory = SelectContactsViewModel.Factory::class)
class SelectContactsViewModel @AssistedInject constructor(
private val storage: StorageProtocol,
private val configFactory: ConfigFactory,
@Assisted private val excludingAccountIDs: Set<String>,
@ApplicationContext private val appContext: Context,
@Assisted private val excludingAccountIDs: Set<AccountId>,
@Assisted private val scope: CoroutineScope
) : ViewModel() {
// Input: The search query
private val mutableSearchQuery = MutableStateFlow("")
// Input: The selected contact account IDs
private val mutableSelectedContactAccountIDs = MutableStateFlow(emptySet<String>())
private val mutableSelectedContactAccountIDs = MutableStateFlow(emptySet<AccountId>())
// Output: The search query
val searchQuery: StateFlow<String> get() = mutableSearchQuery
@ -52,11 +58,11 @@ class SelectContactsViewModel @AssistedInject constructor(
).stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
// Output
val currentSelected: Set<Contact>
val currentSelected: Set<AccountId>
get() = contacts.value
.asSequence()
.filter { it.selected }
.map { it.contact }
.map { it.accountID }
.toSet()
override fun onCleared() {
@ -71,34 +77,32 @@ class SelectContactsViewModel @AssistedInject constructor(
.map {
withContext(Dispatchers.Default) {
val allContacts = configFactory.withUserConfigs {
it.contacts.all()
it.contacts.all().filter { it.approvedMe }
}
if (excludingAccountIDs.isEmpty()) {
allContacts
} else {
allContacts.filterNot { it.id in excludingAccountIDs }
}
allContacts.filterNot { AccountId(it.id) in excludingAccountIDs }
}.map { Recipient.from(appContext, Address.fromSerialized(it.id), false) }
}
}
private fun filterContacts(
contacts: Collection<Contact>,
contacts: Collection<Recipient>,
query: String,
selectedAccountIDs: Set<String>
selectedAccountIDs: Set<AccountId>
): List<ContactItem> {
return contacts
.asSequence()
.filter {
query.isBlank() ||
it.name.contains(query, ignoreCase = true) ||
it.nickname.contains(query, ignoreCase = true)
}
.filter { query.isBlank() || it.getSearchName().contains(query, ignoreCase = true) }
.map { contact ->
val accountId = AccountId(contact.address.serialize())
ContactItem(
contact = contact,
selected = selectedAccountIDs.contains(contact.id)
name = contact.getSearchName(),
accountID = accountId,
selected = selectedAccountIDs.contains(accountId),
)
}
.toList()
@ -108,7 +112,7 @@ class SelectContactsViewModel @AssistedInject constructor(
mutableSearchQuery.value = query
}
fun onContactItemClicked(accountID: String) {
fun onContactItemClicked(accountID: AccountId) {
val newSet = mutableSelectedContactAccountIDs.value.toHashSet()
if (!newSet.remove(accountID)) {
newSet.add(accountID)
@ -119,16 +123,14 @@ class SelectContactsViewModel @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(
excludingAccountIDs: Set<String> = emptySet(),
excludingAccountIDs: Set<AccountId> = emptySet(),
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
): SelectContactsViewModel
}
}
data class ContactItem(
val contact: Contact,
val accountID: AccountId,
val name: String,
val selected: Boolean,
) {
val accountID: String get() = contact.id
val name: String get() = contact.displayName.ifEmpty { truncateIdForDisplay(contact.id) }
}
)

View File

@ -41,6 +41,7 @@ import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.ContactItem
import org.thoughtcrime.securesms.ui.Avatar
import org.thoughtcrime.securesms.ui.theme.LocalColors
@ -69,7 +70,7 @@ fun GroupMinimumVersionBanner(modifier: Modifier = Modifier) {
fun LazyListScope.multiSelectMemberList(
contacts: List<ContactItem>,
modifier: Modifier = Modifier,
onContactItemClicked: (accountId: String) -> Unit,
onContactItemClicked: (accountId: AccountId) -> Unit,
enabled: Boolean = true,
) {
items(contacts) { contact ->
@ -120,7 +121,7 @@ fun RowScope.MemberName(
@Composable
fun RowScope.ContactPhoto(sessionId: String) {
fun RowScope.ContactPhoto(sessionId: AccountId) {
return if (LocalInspectionMode.current) {
Image(
painterResource(id = R.drawable.ic_profile_default),
@ -136,7 +137,7 @@ fun RowScope.ContactPhoto(sessionId: String) {
val context = LocalContext.current
// Ideally we migrate to something that doesn't require recipient, or get contact photo another way
val recipient = remember(sessionId) {
Recipient.from(context, Address.fromSerialized(sessionId), false)
Recipient.from(context, Address.fromSerialized(sessionId.hexString), false)
}
Avatar(recipient, modifier = Modifier.size(48.dp))
}
@ -153,11 +154,13 @@ fun PreviewMemberList() {
multiSelectMemberList(
contacts = listOf(
ContactItem(
Contact(random, "Person"),
accountID = AccountId(random),
name = "Person",
selected = false,
),
ContactItem(
Contact(random, "Cow"),
accountID = AccountId(random),
name = "Cow",
selected = true,
)
),

View File

@ -21,7 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.Contact
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.ContactItem
import org.thoughtcrime.securesms.groups.CreateGroupEvent
import org.thoughtcrime.securesms.groups.CreateGroupViewModel
@ -83,7 +83,7 @@ fun CreateGroup(
groupNameError: String,
contactSearchQuery: String,
onContactSearchQueryChanged: (String) -> Unit,
onContactItemClicked: (accountID: String) -> Unit,
onContactItemClicked: (accountID: AccountId) -> Unit,
showLoading: Boolean,
items: List<ContactItem>,
onCreateClicked: () -> Unit,
@ -144,8 +144,8 @@ private fun CreateGroupPreview(
) {
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
val previewMembers = listOf(
ContactItem(Contact(random, name = "Alice"), false),
ContactItem(Contact(random, name = "Bob"), true),
ContactItem(accountID = AccountId(random), name = "Alice", false),
ContactItem(accountID = AccountId(random), name = "Bob", true),
)
PreviewTheme {

View File

@ -114,10 +114,10 @@ private object RouteEditGroup
fun EditGroup(
onBackClick: () -> Unit,
onAddMemberClick: () -> Unit,
onResendInviteClick: (accountId: String) -> Unit,
onResendPromotionClick: (accountId: String) -> Unit,
onPromoteClick: (accountId: String) -> Unit,
onRemoveClick: (accountId: String, removeMessages: Boolean) -> Unit,
onResendInviteClick: (accountId: AccountId) -> Unit,
onResendPromotionClick: (accountId: AccountId) -> Unit,
onPromoteClick: (accountId: AccountId) -> Unit,
onRemoveClick: (accountId: AccountId, removeMessages: Boolean) -> Unit,
onEditingNameValueChanged: (String) -> Unit,
editingName: String?,
onEditNameClicked: () -> Unit,
@ -300,7 +300,7 @@ fun EditGroup(
@Composable
private fun ConfirmRemovingMemberDialog(
onConfirmed: (accountId: String, removeMessages: Boolean) -> Unit,
onConfirmed: (accountId: AccountId, removeMessages: Boolean) -> Unit,
onDismissRequest: () -> Unit,
member: GroupMemberState,
groupName: String,
@ -393,7 +393,7 @@ private fun MemberModalBottomSheetOptionItem(
@Composable
private fun MemberItem(
onClick: (accountId: String) -> Unit,
onClick: (accountId: AccountId) -> Unit,
member: GroupMemberState,
modifier: Modifier = Modifier,
) {
@ -445,7 +445,7 @@ private fun MemberItem(
private fun EditGroupPreview() {
PreviewTheme {
val oneMember = GroupMemberState(
accountId = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"),
name = "Test User",
status = "Invited",
highlightStatus = false,
@ -455,7 +455,7 @@ private fun EditGroupPreview() {
canResendPromotion = false,
)
val twoMember = GroupMemberState(
accountId = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235",
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"),
name = "Test User 2",
status = "Promote failed",
highlightStatus = true,
@ -465,7 +465,7 @@ private fun EditGroupPreview() {
canResendPromotion = false,
)
val threeMember = GroupMemberState(
accountId = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236",
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"),
name = "Test User 3",
status = "",
highlightStatus = false,

View File

@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.serialization.Serializable
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.Contact
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.ContactItem
import org.thoughtcrime.securesms.groups.SelectContactsViewModel
import org.thoughtcrime.securesms.ui.SearchBar
@ -38,8 +38,8 @@ object RouteSelectContacts
@Composable
fun SelectContactsScreen(
excludingAccountIDs: Set<String> = emptySet(),
onDoneClicked: (selectedContacts: Set<Contact>) -> Unit,
excludingAccountIDs: Set<AccountId> = emptySet(),
onDoneClicked: (selectedContacts: Set<AccountId>) -> Unit,
onBackClicked: () -> Unit,
) {
val viewModel = hiltViewModel<SelectContactsViewModel, SelectContactsViewModel.Factory> { factory ->
@ -60,7 +60,7 @@ fun SelectContactsScreen(
@Composable
fun SelectContacts(
contacts: List<ContactItem>,
onContactItemClicked: (accountId: String) -> Unit,
onContactItemClicked: (accountId: AccountId) -> Unit,
searchQuery: String,
onSearchQueryChanged: (String) -> Unit,
onDoneClicked: () -> Unit,
@ -117,15 +117,19 @@ fun SelectContacts(
@Preview
@Composable
private fun PreviewSelectContacts() {
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
PreviewTheme {
SelectContacts(
contacts = listOf(
ContactItem(
contact = Contact(id = "123", name = "User 1"),
accountID = AccountId(random),
name = "User 1",
selected = false,
),
ContactItem(
contact = Contact(id = "124", name = "User 2"),
accountID = AccountId(random),
name = "User 2",
selected = true,
),
),

View File

@ -1,6 +1,5 @@
package org.session.libsession.messaging.groups
import network.loki.messenger.libsession_util.util.Contact
import org.session.libsession.messaging.messages.control.GroupUpdated
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
@ -14,7 +13,7 @@ interface GroupManagerV2 {
suspend fun createGroup(
groupName: String,
groupDescription: String,
members: Set<Contact>
members: Set<AccountId>
): Recipient
suspend fun inviteMembers(

View File

@ -6,6 +6,8 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
@ -49,23 +51,31 @@ class ClosedGroupPoller(
private const val TAG = "ClosedGroupPoller"
}
private var job: Job? = null
sealed interface State
data object IdleState : State
data class StartedState(internal val job: Job, val hadAtLeastOneSuccessfulPoll: Boolean = false) : State
private val mutableState = MutableStateFlow<State>(IdleState)
val state: StateFlow<State> get() = mutableState
fun start() {
if (job?.isActive == true) return // already started, don't restart
if ((state.value as? StartedState)?.job?.isActive == true) return // already started, don't restart
Log.d(TAG, "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}")
job?.cancel()
job = scope.launch(executor) {
val job = scope.launch(executor) {
while (isActive) {
try {
val swarmNodes = SnodeAPI.fetchSwarmNodes(closedGroupSessionId.hexString).toMutableSet()
val swarmNodes =
SnodeAPI.fetchSwarmNodes(closedGroupSessionId.hexString).toMutableSet()
var currentSnode: Snode? = null
while (isActive) {
if (currentSnode == null) {
check(swarmNodes.isNotEmpty()) { "No more swarm nodes found" }
Log.d(TAG, "No current snode, getting a new one. Remaining in pool = ${swarmNodes.size - 1}")
Log.d(
TAG,
"No current snode, getting a new one. Remaining in pool = ${swarmNodes.size - 1}"
)
currentSnode = swarmNodes.random()
swarmNodes.remove(currentSnode)
}
@ -97,11 +107,17 @@ class ClosedGroupPoller(
}
}
}
mutableState.value = StartedState(job = job)
job.invokeOnCompletion {
mutableState.value = IdleState
}
}
fun stop() {
job?.cancel()
job = null
Log.d(TAG, "Stopping closed group poller for ${closedGroupSessionId.hexString.take(4)}")
(state.value as? StartedState)?.job?.cancel()
}
private suspend fun poll(snode: Snode): Unit = supervisorScope {
@ -236,6 +252,12 @@ class ClosedGroupPoller(
}
}
}
// Update the state to indicate that we had at least one successful poll
val currentState = state.value as? StartedState
if (currentState != null && !currentState.hadAtLeastOneSuccessfulPoll) {
mutableState.value = currentState.copy(hadAtLeastOneSuccessfulPoll = true)
}
}
private fun RetrieveMessageResponse.Message.toConfigMessage(): ConfigMessage {