mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-28 20:45:17 +00:00
Improvement
This commit is contained in:
parent
b8e98ec9ed
commit
4536aca327
@ -1,8 +1,10 @@
|
|||||||
package org.thoughtcrime.securesms.groups
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@ -19,6 +21,7 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class CreateGroupViewModel @Inject constructor(
|
class CreateGroupViewModel @Inject constructor(
|
||||||
configFactory: ConfigFactory,
|
configFactory: ConfigFactory,
|
||||||
|
@ApplicationContext appContext: Context,
|
||||||
private val storage: StorageProtocol,
|
private val storage: StorageProtocol,
|
||||||
private val groupManagerV2: GroupManagerV2,
|
private val groupManagerV2: GroupManagerV2,
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
@ -28,6 +31,7 @@ class CreateGroupViewModel @Inject constructor(
|
|||||||
configFactory = configFactory,
|
configFactory = configFactory,
|
||||||
excludingAccountIDs = emptySet(),
|
excludingAccountIDs = emptySet(),
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
appContext = appContext,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Input: group name
|
// Input: group name
|
||||||
|
@ -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>
|
|
||||||
)
|
|
@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
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.GroupDisplayInfo
|
||||||
import network.loki.messenger.libsession_util.util.GroupMember
|
import network.loki.messenger.libsession_util.util.GroupMember
|
||||||
import org.session.libsession.database.StorageProtocol
|
import org.session.libsession.database.StorageProtocol
|
||||||
@ -67,9 +66,9 @@ class EditGroupViewModel @AssistedInject constructor(
|
|||||||
memberPendingState
|
memberPendingState
|
||||||
) { _, pending ->
|
) { _, pending ->
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val currentUserId = checkNotNull(storage.getUserPublicKey()) {
|
val currentUserId = AccountId(checkNotNull(storage.getUserPublicKey()) {
|
||||||
"User public key is null"
|
"User public key is null"
|
||||||
}
|
})
|
||||||
|
|
||||||
val displayInfo = storage.getClosedGroupDisplayInfo(groupId.hexString)
|
val displayInfo = storage.getClosedGroupDisplayInfo(groupId.hexString)
|
||||||
?: return@withContext null
|
?: return@withContext null
|
||||||
@ -118,21 +117,21 @@ class EditGroupViewModel @AssistedInject constructor(
|
|||||||
val error: StateFlow<String?> get() = mutableError
|
val error: StateFlow<String?> get() = mutableError
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
val excludingAccountIDsFromContactSelection: Set<String>
|
val excludingAccountIDsFromContactSelection: Set<AccountId>
|
||||||
get() = groupInfo.value?.second?.mapTo(hashSetOf()) { it.accountId }.orEmpty()
|
get() = groupInfo.value?.second?.mapTo(hashSetOf()) { it.accountId }.orEmpty()
|
||||||
|
|
||||||
private fun createGroupMember(
|
private fun createGroupMember(
|
||||||
member: GroupMember,
|
member: GroupMember,
|
||||||
myAccountId: String,
|
myAccountId: AccountId,
|
||||||
amIAdmin: Boolean,
|
amIAdmin: Boolean,
|
||||||
pendingState: MemberPendingState?
|
pendingState: MemberPendingState?
|
||||||
): GroupMemberState {
|
): GroupMemberState {
|
||||||
var status = ""
|
var status = ""
|
||||||
var highlightStatus = false
|
var highlightStatus = false
|
||||||
var name = member.name.orEmpty()
|
var name = member.name.orEmpty().ifEmpty { member.sessionId }
|
||||||
|
|
||||||
when {
|
when {
|
||||||
member.sessionId == myAccountId -> {
|
member.sessionId == myAccountId.hexString -> {
|
||||||
name = context.getString(R.string.you)
|
name = context.getString(R.string.you)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,81 +163,81 @@ class EditGroupViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return GroupMemberState(
|
return GroupMemberState(
|
||||||
accountId = member.sessionId,
|
accountId = AccountId(member.sessionId),
|
||||||
name = name,
|
name = name,
|
||||||
canRemove = amIAdmin && member.sessionId != myAccountId && !member.isAdminOrBeingPromoted,
|
canRemove = amIAdmin && member.sessionId != myAccountId.hexString && !member.isAdminOrBeingPromoted,
|
||||||
canPromote = amIAdmin && member.sessionId != myAccountId && !member.isAdminOrBeingPromoted,
|
canPromote = amIAdmin && member.sessionId != myAccountId.hexString && !member.isAdminOrBeingPromoted,
|
||||||
canResendPromotion = amIAdmin && member.sessionId != myAccountId && member.promotionFailed,
|
canResendPromotion = amIAdmin && member.sessionId != myAccountId.hexString && member.promotionFailed,
|
||||||
canResendInvite = amIAdmin && member.sessionId != myAccountId &&
|
canResendInvite = amIAdmin && member.sessionId != myAccountId.hexString &&
|
||||||
(member.inviteFailed || member.invitePending),
|
(member.inviteFailed || member.invitePending),
|
||||||
status = status,
|
status = status,
|
||||||
highlightStatus = highlightStatus
|
highlightStatus = highlightStatus
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortMembers(members: MutableList<GroupMember>, currentUserId: String) {
|
private fun sortMembers(members: MutableList<GroupMember>, currentUserId: AccountId) {
|
||||||
members.sortWith(
|
members.sortWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
{ !it.inviteFailed }, // Failed invite comes first (as false value is less than true)
|
{ !it.inviteFailed }, // Failed invite comes first (as false value is less than true)
|
||||||
{ memberPendingState.value[AccountId(it.sessionId)] != MemberPendingState.Inviting }, // "Sending invite" comes first
|
{ memberPendingState.value[AccountId(it.sessionId)] != MemberPendingState.Inviting }, // "Sending invite" comes first
|
||||||
{ !it.invitePending }, // "Invite sent" comes first
|
{ !it.invitePending }, // "Invite sent" comes first
|
||||||
{ !it.isAdminOrBeingPromoted }, // Admins come 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.name }, // Sort by name
|
||||||
{ it.sessionId } // Last resort: sort by account ID
|
{ it.sessionId } // Last resort: sort by account ID
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onContactSelected(contacts: Set<Contact>) {
|
fun onContactSelected(contacts: Set<AccountId>) {
|
||||||
performGroupOperation {
|
performGroupOperation {
|
||||||
try {
|
try {
|
||||||
// Mark the contacts as pending
|
// Mark the contacts as pending
|
||||||
memberPendingState.update { states ->
|
memberPendingState.update { states ->
|
||||||
states + contacts.associate { AccountId(it.id) to MemberPendingState.Inviting }
|
states + contacts.associateWith { MemberPendingState.Inviting }
|
||||||
}
|
}
|
||||||
|
|
||||||
groupManager.inviteMembers(
|
groupManager.inviteMembers(
|
||||||
groupId,
|
groupId,
|
||||||
contacts.map { AccountId(it.id) },
|
contacts.toList(),
|
||||||
shareHistory = false
|
shareHistory = false
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
// Remove pending state (so the real state will be revealed)
|
// 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) {
|
fun onResendInviteClicked(contactSessionId: AccountId) {
|
||||||
onContactSelected(setOf(Contact(contactSessionId)))
|
onContactSelected(setOf(contactSessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPromoteContact(memberSessionId: String) {
|
fun onPromoteContact(memberSessionId: AccountId) {
|
||||||
performGroupOperation {
|
performGroupOperation {
|
||||||
try {
|
try {
|
||||||
memberPendingState.update { states ->
|
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 {
|
} 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 {
|
performGroupOperation {
|
||||||
groupManager.removeMembers(
|
groupManager.removeMembers(
|
||||||
groupAccountId = groupId,
|
groupAccountId = groupId,
|
||||||
removedMembers = listOf(AccountId(contactSessionId)),
|
removedMembers = listOf(contactSessionId),
|
||||||
removeMessages = removeMessages
|
removeMessages = removeMessages
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onResendPromotionClicked(memberSessionId: String) {
|
fun onResendPromotionClicked(memberSessionId: AccountId) {
|
||||||
onPromoteContact(memberSessionId)
|
onPromoteContact(memberSessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,7 +314,7 @@ private enum class MemberPendingState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class GroupMemberState(
|
data class GroupMemberState(
|
||||||
val accountId: String,
|
val accountId: AccountId,
|
||||||
val name: String,
|
val name: String,
|
||||||
val status: String,
|
val status: String,
|
||||||
val highlightStatus: Boolean,
|
val highlightStatus: Boolean,
|
||||||
|
@ -6,9 +6,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISIBLE
|
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.Conversation
|
||||||
import network.loki.messenger.libsession_util.util.GroupInfo
|
import network.loki.messenger.libsession_util.util.GroupInfo
|
||||||
import network.loki.messenger.libsession_util.util.GroupMember
|
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.control.GroupUpdated
|
||||||
import org.session.libsession.messaging.messages.visible.Profile
|
import org.session.libsession.messaging.messages.visible.Profile
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
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.buildDeleteMemberContentSignature
|
||||||
import org.session.libsession.messaging.utilities.MessageAuthentication.buildInfoChangeVerifier
|
import org.session.libsession.messaging.utilities.MessageAuthentication.buildInfoChangeVerifier
|
||||||
import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature
|
import org.session.libsession.messaging.utilities.MessageAuthentication.buildMemberChangeSignature
|
||||||
@ -88,7 +91,7 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
override suspend fun createGroup(
|
override suspend fun createGroup(
|
||||||
groupName: String,
|
groupName: String,
|
||||||
groupDescription: String,
|
groupDescription: String,
|
||||||
members: Set<Contact>
|
members: Set<AccountId>
|
||||||
): Recipient = withContext(dispatcher) {
|
): Recipient = withContext(dispatcher) {
|
||||||
val ourAccountId =
|
val ourAccountId =
|
||||||
requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" }
|
requireNotNull(storage.getUserPublicKey()) { "Our account ID is not available" }
|
||||||
@ -103,9 +106,13 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
.also(configs.userGroups::set)
|
.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 groupId = group.groupAccountId
|
||||||
|
|
||||||
|
val memberAsRecipients = members.map {
|
||||||
|
Recipient.from(application, Address.fromSerialized(it.hexString), false)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
configFactory.withMutableGroupConfigs(groupId) { configs ->
|
configFactory.withMutableGroupConfigs(groupId) { configs ->
|
||||||
// Update group's information
|
// Update group's information
|
||||||
@ -113,12 +120,14 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
configs.groupInfo.setDescription(groupDescription)
|
configs.groupInfo.setDescription(groupDescription)
|
||||||
|
|
||||||
// Add members
|
// Add members
|
||||||
for (member in members) {
|
for (member in memberAsRecipients) {
|
||||||
configs.groupMembers.set(
|
configs.groupMembers.set(
|
||||||
GroupMember(
|
GroupMember(
|
||||||
sessionId = member.id,
|
sessionId = member.address.serialize(),
|
||||||
name = member.name,
|
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
|
inviteStatus = INVITE_STATUS_SENT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -165,10 +174,13 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
JobQueue.shared.add(
|
JobQueue.shared.add(
|
||||||
InviteContactsJob(
|
InviteContactsJob(
|
||||||
groupSessionId = groupId.hexString,
|
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
|
recipient
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to create group", e)
|
Log.e(TAG, "Failed to create group", e)
|
||||||
@ -223,7 +235,7 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
configs.groupMembers.set(toSet)
|
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) {
|
if (shareHistory) {
|
||||||
val memberKey = configs.groupKeys.supplementFor(newMembers.map { it.hexString })
|
val memberKey = configs.groupKeys.supplementFor(newMembers.map { it.hexString })
|
||||||
batchRequests.add(
|
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 timestamp = clock.currentTimeMills()
|
||||||
val signature = SodiumUtilities.sign(
|
val signature = SodiumUtilities.sign(
|
||||||
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.ADDED, timestamp),
|
buildMemberChangeSignature(GroupUpdateMemberChangeMessage.Type.ADDED, timestamp),
|
||||||
@ -284,8 +308,11 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
).apply { this.sentTimestamp = timestamp }
|
).apply { this.sentTimestamp = timestamp }
|
||||||
MessageSender.send(updatedMessage, Destination.ClosedGroup(group.hexString), false).await()
|
MessageSender.send(updatedMessage, Destination.ClosedGroup(group.hexString), false)
|
||||||
storage.insertGroupInfoChange(updatedMessage, group)
|
|
||||||
|
if (insertLocally) {
|
||||||
|
storage.insertGroupInfoChange(updatedMessage, group)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun removeMembers(
|
override suspend fun removeMembers(
|
||||||
@ -529,16 +556,15 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
lokiDatabase.deleteGroupInviteReferrer(threadId)
|
lokiDatabase.deleteGroupInviteReferrer(threadId)
|
||||||
|
|
||||||
if (approved) {
|
if (approved) {
|
||||||
approveGroupInvite(group, threadId)
|
approveGroupInvite(group)
|
||||||
} else {
|
} else {
|
||||||
configFactory.withMutableUserConfigs { it.userGroups.eraseClosedGroup(groupId.hexString) }
|
configFactory.withMutableUserConfigs { it.userGroups.eraseClosedGroup(groupId.hexString) }
|
||||||
storage.deleteConversation(threadId)
|
storage.deleteConversation(threadId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun approveGroupInvite(
|
private suspend fun approveGroupInvite(
|
||||||
group: GroupInfo.ClosedGroupInfo,
|
group: GroupInfo.ClosedGroupInfo,
|
||||||
threadId: Long,
|
|
||||||
) {
|
) {
|
||||||
val key = requireNotNull(storage.getUserPublicKey()) {
|
val key = requireNotNull(storage.getUserPublicKey()) {
|
||||||
"Our account ID is not available"
|
"Our account ID is not available"
|
||||||
@ -549,6 +575,15 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
configs.userGroups.set(group.copy(invited = false))
|
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) {
|
if (group.adminKey == null) {
|
||||||
// Send an invite response to the group if we are invited as a regular member
|
// Send an invite response to the group if we are invited as a regular member
|
||||||
val inviteResponse = GroupUpdateInviteResponseMessage.newBuilder()
|
val inviteResponse = GroupUpdateInviteResponseMessage.newBuilder()
|
||||||
@ -572,8 +607,6 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pollerFactory.pollerFor(group.groupAccountId)?.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun handleInvitation(
|
override suspend fun handleInvitation(
|
||||||
@ -656,7 +689,7 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
* @param inviter the invite message sender
|
* @param inviter the invite message sender
|
||||||
* @return The newly created group info if the invitation is processed, null otherwise.
|
* @return The newly created group info if the invitation is processed, null otherwise.
|
||||||
*/
|
*/
|
||||||
private fun handleInvitation(
|
private suspend fun handleInvitation(
|
||||||
groupId: AccountId,
|
groupId: AccountId,
|
||||||
groupName: String,
|
groupName: String,
|
||||||
authDataOrAdminKey: ByteArray,
|
authDataOrAdminKey: ByteArray,
|
||||||
@ -691,8 +724,9 @@ class GroupManagerV2Impl @Inject constructor(
|
|||||||
val groupThreadId = storage.getOrCreateThreadIdFor(recipient.address)
|
val groupThreadId = storage.getOrCreateThreadIdFor(recipient.address)
|
||||||
storage.setRecipientApprovedMe(recipient, true)
|
storage.setRecipientApprovedMe(recipient, true)
|
||||||
storage.setRecipientApproved(recipient, shouldAutoApprove)
|
storage.setRecipientApproved(recipient, shouldAutoApprove)
|
||||||
|
|
||||||
if (shouldAutoApprove) {
|
if (shouldAutoApprove) {
|
||||||
approveGroupInvite(closedGroupInfo, groupThreadId)
|
approveGroupInvite(closedGroupInfo)
|
||||||
} else {
|
} else {
|
||||||
lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString)
|
lokiDatabase.addGroupInviteReferrer(groupThreadId, inviter.hexString)
|
||||||
storage.insertGroupInviteControlMessage(
|
storage.insertGroupInviteControlMessage(
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package org.thoughtcrime.securesms.groups
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
@ -21,24 +23,28 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.libsession_util.util.Contact
|
|
||||||
import org.session.libsession.database.StorageProtocol
|
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.libsession.utilities.truncateIdForDisplay
|
||||||
|
import org.session.libsignal.utilities.AccountId
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import org.thoughtcrime.securesms.home.search.getSearchName
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
@HiltViewModel(assistedFactory = SelectContactsViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = SelectContactsViewModel.Factory::class)
|
||||||
class SelectContactsViewModel @AssistedInject constructor(
|
class SelectContactsViewModel @AssistedInject constructor(
|
||||||
private val storage: StorageProtocol,
|
private val storage: StorageProtocol,
|
||||||
private val configFactory: ConfigFactory,
|
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
|
@Assisted private val scope: CoroutineScope
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
// Input: The search query
|
// Input: The search query
|
||||||
private val mutableSearchQuery = MutableStateFlow("")
|
private val mutableSearchQuery = MutableStateFlow("")
|
||||||
|
|
||||||
// Input: The selected contact account IDs
|
// Input: The selected contact account IDs
|
||||||
private val mutableSelectedContactAccountIDs = MutableStateFlow(emptySet<String>())
|
private val mutableSelectedContactAccountIDs = MutableStateFlow(emptySet<AccountId>())
|
||||||
|
|
||||||
// Output: The search query
|
// Output: The search query
|
||||||
val searchQuery: StateFlow<String> get() = mutableSearchQuery
|
val searchQuery: StateFlow<String> get() = mutableSearchQuery
|
||||||
@ -52,11 +58,11 @@ class SelectContactsViewModel @AssistedInject constructor(
|
|||||||
).stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
).stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
// Output
|
// Output
|
||||||
val currentSelected: Set<Contact>
|
val currentSelected: Set<AccountId>
|
||||||
get() = contacts.value
|
get() = contacts.value
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { it.selected }
|
.filter { it.selected }
|
||||||
.map { it.contact }
|
.map { it.accountID }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
@ -71,34 +77,32 @@ class SelectContactsViewModel @AssistedInject constructor(
|
|||||||
.map {
|
.map {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val allContacts = configFactory.withUserConfigs {
|
val allContacts = configFactory.withUserConfigs {
|
||||||
it.contacts.all()
|
it.contacts.all().filter { it.approvedMe }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (excludingAccountIDs.isEmpty()) {
|
if (excludingAccountIDs.isEmpty()) {
|
||||||
allContacts
|
allContacts
|
||||||
} else {
|
} 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(
|
private fun filterContacts(
|
||||||
contacts: Collection<Contact>,
|
contacts: Collection<Recipient>,
|
||||||
query: String,
|
query: String,
|
||||||
selectedAccountIDs: Set<String>
|
selectedAccountIDs: Set<AccountId>
|
||||||
): List<ContactItem> {
|
): List<ContactItem> {
|
||||||
return contacts
|
return contacts
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter {
|
.filter { query.isBlank() || it.getSearchName().contains(query, ignoreCase = true) }
|
||||||
query.isBlank() ||
|
|
||||||
it.name.contains(query, ignoreCase = true) ||
|
|
||||||
it.nickname.contains(query, ignoreCase = true)
|
|
||||||
}
|
|
||||||
.map { contact ->
|
.map { contact ->
|
||||||
|
val accountId = AccountId(contact.address.serialize())
|
||||||
ContactItem(
|
ContactItem(
|
||||||
contact = contact,
|
name = contact.getSearchName(),
|
||||||
selected = selectedAccountIDs.contains(contact.id)
|
accountID = accountId,
|
||||||
|
selected = selectedAccountIDs.contains(accountId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.toList()
|
.toList()
|
||||||
@ -108,7 +112,7 @@ class SelectContactsViewModel @AssistedInject constructor(
|
|||||||
mutableSearchQuery.value = query
|
mutableSearchQuery.value = query
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onContactItemClicked(accountID: String) {
|
fun onContactItemClicked(accountID: AccountId) {
|
||||||
val newSet = mutableSelectedContactAccountIDs.value.toHashSet()
|
val newSet = mutableSelectedContactAccountIDs.value.toHashSet()
|
||||||
if (!newSet.remove(accountID)) {
|
if (!newSet.remove(accountID)) {
|
||||||
newSet.add(accountID)
|
newSet.add(accountID)
|
||||||
@ -119,16 +123,14 @@ class SelectContactsViewModel @AssistedInject constructor(
|
|||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
excludingAccountIDs: Set<String> = emptySet(),
|
excludingAccountIDs: Set<AccountId> = emptySet(),
|
||||||
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
|
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
|
||||||
): SelectContactsViewModel
|
): SelectContactsViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ContactItem(
|
data class ContactItem(
|
||||||
val contact: Contact,
|
val accountID: AccountId,
|
||||||
|
val name: String,
|
||||||
val selected: Boolean,
|
val selected: Boolean,
|
||||||
) {
|
)
|
||||||
val accountID: String get() = contact.id
|
|
||||||
val name: String get() = contact.displayName.ifEmpty { truncateIdForDisplay(contact.id) }
|
|
||||||
}
|
|
||||||
|
@ -41,6 +41,7 @@ import network.loki.messenger.R
|
|||||||
import network.loki.messenger.libsession_util.util.Contact
|
import network.loki.messenger.libsession_util.util.Contact
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.session.libsignal.utilities.AccountId
|
||||||
import org.thoughtcrime.securesms.groups.ContactItem
|
import org.thoughtcrime.securesms.groups.ContactItem
|
||||||
import org.thoughtcrime.securesms.ui.Avatar
|
import org.thoughtcrime.securesms.ui.Avatar
|
||||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||||
@ -69,7 +70,7 @@ fun GroupMinimumVersionBanner(modifier: Modifier = Modifier) {
|
|||||||
fun LazyListScope.multiSelectMemberList(
|
fun LazyListScope.multiSelectMemberList(
|
||||||
contacts: List<ContactItem>,
|
contacts: List<ContactItem>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onContactItemClicked: (accountId: String) -> Unit,
|
onContactItemClicked: (accountId: AccountId) -> Unit,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
items(contacts) { contact ->
|
items(contacts) { contact ->
|
||||||
@ -120,7 +121,7 @@ fun RowScope.MemberName(
|
|||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RowScope.ContactPhoto(sessionId: String) {
|
fun RowScope.ContactPhoto(sessionId: AccountId) {
|
||||||
return if (LocalInspectionMode.current) {
|
return if (LocalInspectionMode.current) {
|
||||||
Image(
|
Image(
|
||||||
painterResource(id = R.drawable.ic_profile_default),
|
painterResource(id = R.drawable.ic_profile_default),
|
||||||
@ -136,7 +137,7 @@ fun RowScope.ContactPhoto(sessionId: String) {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
// Ideally we migrate to something that doesn't require recipient, or get contact photo another way
|
// Ideally we migrate to something that doesn't require recipient, or get contact photo another way
|
||||||
val recipient = remember(sessionId) {
|
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))
|
Avatar(recipient, modifier = Modifier.size(48.dp))
|
||||||
}
|
}
|
||||||
@ -153,11 +154,13 @@ fun PreviewMemberList() {
|
|||||||
multiSelectMemberList(
|
multiSelectMemberList(
|
||||||
contacts = listOf(
|
contacts = listOf(
|
||||||
ContactItem(
|
ContactItem(
|
||||||
Contact(random, "Person"),
|
accountID = AccountId(random),
|
||||||
|
name = "Person",
|
||||||
selected = false,
|
selected = false,
|
||||||
),
|
),
|
||||||
ContactItem(
|
ContactItem(
|
||||||
Contact(random, "Cow"),
|
accountID = AccountId(random),
|
||||||
|
name = "Cow",
|
||||||
selected = true,
|
selected = true,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -21,7 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import network.loki.messenger.R
|
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.ContactItem
|
||||||
import org.thoughtcrime.securesms.groups.CreateGroupEvent
|
import org.thoughtcrime.securesms.groups.CreateGroupEvent
|
||||||
import org.thoughtcrime.securesms.groups.CreateGroupViewModel
|
import org.thoughtcrime.securesms.groups.CreateGroupViewModel
|
||||||
@ -83,7 +83,7 @@ fun CreateGroup(
|
|||||||
groupNameError: String,
|
groupNameError: String,
|
||||||
contactSearchQuery: String,
|
contactSearchQuery: String,
|
||||||
onContactSearchQueryChanged: (String) -> Unit,
|
onContactSearchQueryChanged: (String) -> Unit,
|
||||||
onContactItemClicked: (accountID: String) -> Unit,
|
onContactItemClicked: (accountID: AccountId) -> Unit,
|
||||||
showLoading: Boolean,
|
showLoading: Boolean,
|
||||||
items: List<ContactItem>,
|
items: List<ContactItem>,
|
||||||
onCreateClicked: () -> Unit,
|
onCreateClicked: () -> Unit,
|
||||||
@ -144,8 +144,8 @@ private fun CreateGroupPreview(
|
|||||||
) {
|
) {
|
||||||
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
|
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
|
||||||
val previewMembers = listOf(
|
val previewMembers = listOf(
|
||||||
ContactItem(Contact(random, name = "Alice"), false),
|
ContactItem(accountID = AccountId(random), name = "Alice", false),
|
||||||
ContactItem(Contact(random, name = "Bob"), true),
|
ContactItem(accountID = AccountId(random), name = "Bob", true),
|
||||||
)
|
)
|
||||||
|
|
||||||
PreviewTheme {
|
PreviewTheme {
|
||||||
|
@ -114,10 +114,10 @@ private object RouteEditGroup
|
|||||||
fun EditGroup(
|
fun EditGroup(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onAddMemberClick: () -> Unit,
|
onAddMemberClick: () -> Unit,
|
||||||
onResendInviteClick: (accountId: String) -> Unit,
|
onResendInviteClick: (accountId: AccountId) -> Unit,
|
||||||
onResendPromotionClick: (accountId: String) -> Unit,
|
onResendPromotionClick: (accountId: AccountId) -> Unit,
|
||||||
onPromoteClick: (accountId: String) -> Unit,
|
onPromoteClick: (accountId: AccountId) -> Unit,
|
||||||
onRemoveClick: (accountId: String, removeMessages: Boolean) -> Unit,
|
onRemoveClick: (accountId: AccountId, removeMessages: Boolean) -> Unit,
|
||||||
onEditingNameValueChanged: (String) -> Unit,
|
onEditingNameValueChanged: (String) -> Unit,
|
||||||
editingName: String?,
|
editingName: String?,
|
||||||
onEditNameClicked: () -> Unit,
|
onEditNameClicked: () -> Unit,
|
||||||
@ -300,7 +300,7 @@ fun EditGroup(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConfirmRemovingMemberDialog(
|
private fun ConfirmRemovingMemberDialog(
|
||||||
onConfirmed: (accountId: String, removeMessages: Boolean) -> Unit,
|
onConfirmed: (accountId: AccountId, removeMessages: Boolean) -> Unit,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
member: GroupMemberState,
|
member: GroupMemberState,
|
||||||
groupName: String,
|
groupName: String,
|
||||||
@ -393,7 +393,7 @@ private fun MemberModalBottomSheetOptionItem(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MemberItem(
|
private fun MemberItem(
|
||||||
onClick: (accountId: String) -> Unit,
|
onClick: (accountId: AccountId) -> Unit,
|
||||||
member: GroupMemberState,
|
member: GroupMemberState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@ -445,7 +445,7 @@ private fun MemberItem(
|
|||||||
private fun EditGroupPreview() {
|
private fun EditGroupPreview() {
|
||||||
PreviewTheme {
|
PreviewTheme {
|
||||||
val oneMember = GroupMemberState(
|
val oneMember = GroupMemberState(
|
||||||
accountId = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
|
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"),
|
||||||
name = "Test User",
|
name = "Test User",
|
||||||
status = "Invited",
|
status = "Invited",
|
||||||
highlightStatus = false,
|
highlightStatus = false,
|
||||||
@ -455,7 +455,7 @@ private fun EditGroupPreview() {
|
|||||||
canResendPromotion = false,
|
canResendPromotion = false,
|
||||||
)
|
)
|
||||||
val twoMember = GroupMemberState(
|
val twoMember = GroupMemberState(
|
||||||
accountId = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235",
|
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"),
|
||||||
name = "Test User 2",
|
name = "Test User 2",
|
||||||
status = "Promote failed",
|
status = "Promote failed",
|
||||||
highlightStatus = true,
|
highlightStatus = true,
|
||||||
@ -465,7 +465,7 @@ private fun EditGroupPreview() {
|
|||||||
canResendPromotion = false,
|
canResendPromotion = false,
|
||||||
)
|
)
|
||||||
val threeMember = GroupMemberState(
|
val threeMember = GroupMemberState(
|
||||||
accountId = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236",
|
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"),
|
||||||
name = "Test User 3",
|
name = "Test User 3",
|
||||||
status = "",
|
status = "",
|
||||||
highlightStatus = false,
|
highlightStatus = false,
|
||||||
|
@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import network.loki.messenger.R
|
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.ContactItem
|
||||||
import org.thoughtcrime.securesms.groups.SelectContactsViewModel
|
import org.thoughtcrime.securesms.groups.SelectContactsViewModel
|
||||||
import org.thoughtcrime.securesms.ui.SearchBar
|
import org.thoughtcrime.securesms.ui.SearchBar
|
||||||
@ -38,8 +38,8 @@ object RouteSelectContacts
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectContactsScreen(
|
fun SelectContactsScreen(
|
||||||
excludingAccountIDs: Set<String> = emptySet(),
|
excludingAccountIDs: Set<AccountId> = emptySet(),
|
||||||
onDoneClicked: (selectedContacts: Set<Contact>) -> Unit,
|
onDoneClicked: (selectedContacts: Set<AccountId>) -> Unit,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val viewModel = hiltViewModel<SelectContactsViewModel, SelectContactsViewModel.Factory> { factory ->
|
val viewModel = hiltViewModel<SelectContactsViewModel, SelectContactsViewModel.Factory> { factory ->
|
||||||
@ -60,7 +60,7 @@ fun SelectContactsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SelectContacts(
|
fun SelectContacts(
|
||||||
contacts: List<ContactItem>,
|
contacts: List<ContactItem>,
|
||||||
onContactItemClicked: (accountId: String) -> Unit,
|
onContactItemClicked: (accountId: AccountId) -> Unit,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
onSearchQueryChanged: (String) -> Unit,
|
onSearchQueryChanged: (String) -> Unit,
|
||||||
onDoneClicked: () -> Unit,
|
onDoneClicked: () -> Unit,
|
||||||
@ -117,15 +117,19 @@ fun SelectContacts(
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun PreviewSelectContacts() {
|
private fun PreviewSelectContacts() {
|
||||||
|
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
|
||||||
|
|
||||||
PreviewTheme {
|
PreviewTheme {
|
||||||
SelectContacts(
|
SelectContacts(
|
||||||
contacts = listOf(
|
contacts = listOf(
|
||||||
ContactItem(
|
ContactItem(
|
||||||
contact = Contact(id = "123", name = "User 1"),
|
accountID = AccountId(random),
|
||||||
|
name = "User 1",
|
||||||
selected = false,
|
selected = false,
|
||||||
),
|
),
|
||||||
ContactItem(
|
ContactItem(
|
||||||
contact = Contact(id = "124", name = "User 2"),
|
accountID = AccountId(random),
|
||||||
|
name = "User 2",
|
||||||
selected = true,
|
selected = true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package org.session.libsession.messaging.groups
|
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.messaging.messages.control.GroupUpdated
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
|
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage
|
||||||
@ -14,7 +13,7 @@ interface GroupManagerV2 {
|
|||||||
suspend fun createGroup(
|
suspend fun createGroup(
|
||||||
groupName: String,
|
groupName: String,
|
||||||
groupDescription: String,
|
groupDescription: String,
|
||||||
members: Set<Contact>
|
members: Set<AccountId>
|
||||||
): Recipient
|
): Recipient
|
||||||
|
|
||||||
suspend fun inviteMembers(
|
suspend fun inviteMembers(
|
||||||
|
@ -6,6 +6,8 @@ import kotlinx.coroutines.Deferred
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
@ -49,23 +51,31 @@ class ClosedGroupPoller(
|
|||||||
private const val TAG = "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() {
|
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)}")
|
Log.d(TAG, "Starting closed group poller for ${closedGroupSessionId.hexString.take(4)}")
|
||||||
job?.cancel()
|
val job = scope.launch(executor) {
|
||||||
job = scope.launch(executor) {
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
val swarmNodes = SnodeAPI.fetchSwarmNodes(closedGroupSessionId.hexString).toMutableSet()
|
val swarmNodes =
|
||||||
|
SnodeAPI.fetchSwarmNodes(closedGroupSessionId.hexString).toMutableSet()
|
||||||
var currentSnode: Snode? = null
|
var currentSnode: Snode? = null
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
if (currentSnode == null) {
|
if (currentSnode == null) {
|
||||||
check(swarmNodes.isNotEmpty()) { "No more swarm nodes found" }
|
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()
|
currentSnode = swarmNodes.random()
|
||||||
swarmNodes.remove(currentSnode)
|
swarmNodes.remove(currentSnode)
|
||||||
}
|
}
|
||||||
@ -97,11 +107,17 @@ class ClosedGroupPoller(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutableState.value = StartedState(job = job)
|
||||||
|
|
||||||
|
job.invokeOnCompletion {
|
||||||
|
mutableState.value = IdleState
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
job?.cancel()
|
Log.d(TAG, "Stopping closed group poller for ${closedGroupSessionId.hexString.take(4)}")
|
||||||
job = null
|
(state.value as? StartedState)?.job?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun poll(snode: Snode): Unit = supervisorScope {
|
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 {
|
private fun RetrieveMessageResponse.Message.toConfigMessage(): ConfigMessage {
|
||||||
|
Loading…
Reference in New Issue
Block a user