diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt index d7050ade31..5a20d7be9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupInviteViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupInviteViewModel.kt deleted file mode 100644 index c997575656..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupInviteViewModel.kt +++ /dev/null @@ -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, - val allContacts: Set -) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt index bade85203c..18d81bab97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditGroupViewModel.kt @@ -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 get() = mutableError // Output: - val excludingAccountIDsFromContactSelection: Set + val excludingAccountIDsFromContactSelection: Set 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, currentUserId: String) { + private fun sortMembers(members: MutableList, 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) { + fun onContactSelected(contacts: Set) { 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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index bb4620efa9..31411b931f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -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 + members: Set ): 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, + 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() + .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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt index 1d5ffa65e2..fc2d756087 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/SelectContactsViewModel.kt @@ -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, + @ApplicationContext private val appContext: Context, + @Assisted private val excludingAccountIDs: Set, @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()) + private val mutableSelectedContactAccountIDs = MutableStateFlow(emptySet()) // Output: The search query val searchQuery: StateFlow get() = mutableSearchQuery @@ -52,11 +58,11 @@ class SelectContactsViewModel @AssistedInject constructor( ).stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) // Output - val currentSelected: Set + val currentSelected: Set 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, + contacts: Collection, query: String, - selectedAccountIDs: Set + selectedAccountIDs: Set ): List { 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 = emptySet(), + excludingAccountIDs: Set = 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) } -} \ No newline at end of file +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt index abae0e7c80..809656332c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt @@ -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, 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, ) ), diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt index 9a6e41556c..a5bea99e6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroupScreen.kt @@ -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, 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt index 816bd8f364..a8bd0ec0c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupScreen.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContactsScreen.kt index 882faf2112..305eb6fc93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContactsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContactsScreen.kt @@ -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 = emptySet(), - onDoneClicked: (selectedContacts: Set) -> Unit, + excludingAccountIDs: Set = emptySet(), + onDoneClicked: (selectedContacts: Set) -> Unit, onBackClicked: () -> Unit, ) { val viewModel = hiltViewModel { factory -> @@ -60,7 +60,7 @@ fun SelectContactsScreen( @Composable fun SelectContacts( contacts: List, - 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, ), ), diff --git a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt index 8e40f9c9a8..4468ce0fb2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/groups/GroupManagerV2.kt @@ -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 + members: Set ): Recipient suspend fun inviteMembers( diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt index 1fad1d48ae..f13cafaf6a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPoller.kt @@ -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(IdleState) + val state: StateFlow 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 {