Merge remote-tracking branch 'origin/release/1.21.0' into merge-release-1.21.0

pull/1710/head
SessionHero01 3 months ago
commit 20fc0a2d2d
No known key found for this signature in database

@ -11,6 +11,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
@ -33,6 +34,7 @@ import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigPushResult import org.session.libsession.utilities.ConfigPushResult
import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.UserConfigType import org.session.libsession.utilities.UserConfigType
import org.session.libsession.utilities.getGroup import org.session.libsession.utilities.getGroup
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
@ -43,6 +45,7 @@ import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.retryWithUniformInterval import org.session.libsignal.utilities.retryWithUniformInterval
import org.thoughtcrime.securesms.util.InternetConnectivity import org.thoughtcrime.securesms.util.InternetConnectivity
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.log
private const val TAG = "ConfigUploader" private const val TAG = "ConfigUploader"
@ -62,6 +65,7 @@ class ConfigUploader @Inject constructor(
private val storageProtocol: StorageProtocol, private val storageProtocol: StorageProtocol,
private val clock: SnodeClock, private val clock: SnodeClock,
private val internetConnectivity: InternetConnectivity, private val internetConnectivity: InternetConnectivity,
private val textSecurePreferences: TextSecurePreferences,
) { ) {
private var job: Job? = null private var job: Job? = null
@ -82,6 +86,11 @@ class ConfigUploader @Inject constructor(
} }
} }
// A flow that emits true when there's a logged in user
private fun hasLoggedInUser(): Flow<Boolean> = textSecurePreferences.watchLocalNumber()
.map { it != null }
.distinctUntilChanged()
@OptIn(DelicateCoroutinesApi::class, FlowPreview::class, ExperimentalCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class, FlowPreview::class, ExperimentalCoroutinesApi::class)
fun start() { fun start() {
@ -92,41 +101,58 @@ class ConfigUploader @Inject constructor(
// For any of these events, we need to push the user configs: // For any of these events, we need to push the user configs:
// - The onion path has just become available to use // - The onion path has just become available to use
// - The user configs have been modified // - The user configs have been modified
// Also, these events are only relevant when there's a logged in user
val job1 = launch { val job1 = launch {
merge( hasLoggedInUser()
pathBecomesAvailable(), .flatMapLatest { loggedIn ->
configFactory.configUpdateNotifications if (loggedIn) {
.filterIsInstance<ConfigUpdateNotification.UserConfigsModified>() merge(
.debounce(1000L) pathBecomesAvailable(),
).collect { configFactory.configUpdateNotifications
try { .filterIsInstance<ConfigUpdateNotification.UserConfigsModified>()
retryWithUniformInterval { .debounce(1000L)
pushUserConfigChangesIfNeeded() )
} else {
emptyFlow()
}
}
.collect {
try {
retryWithUniformInterval {
pushUserConfigChangesIfNeeded()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to push user configs", e)
} }
} catch (e: Exception) {
Log.e(TAG, "Failed to push user configs", e)
} }
}
} }
val job2 = launch { val job2 = launch {
merge( hasLoggedInUser()
// When the onion request path changes, we need to examine all the groups .flatMapLatest { loggedIn ->
// and push the pending configs for them if (loggedIn) {
pathBecomesAvailable().flatMapLatest { merge(
configFactory.withUserConfigs { configs -> configs.userGroups.allClosedGroupInfo() } // When the onion request path changes, we need to examine all the groups
.asSequence() // and push the pending configs for them
.filter { !it.destroyed && !it.kicked } pathBecomesAvailable().flatMapLatest {
.map { it.groupAccountId } configFactory.withUserConfigs { configs -> configs.userGroups.allClosedGroupInfo() }
.asFlow() .asSequence()
}, .filter { !it.destroyed && !it.kicked }
.map { it.groupAccountId }
// Or, when a group config is updated, we need to push the changes for that group .asFlow()
configFactory.configUpdateNotifications },
.filterIsInstance<ConfigUpdateNotification.GroupConfigsUpdated>()
.map { it.groupId } // Or, when a group config is updated, we need to push the changes for that group
.debounce(1000L) configFactory.configUpdateNotifications
).collect { groupId -> .filterIsInstance<ConfigUpdateNotification.GroupConfigsUpdated>()
.map { it.groupId }
.debounce(1000L)
)
} else {
emptyFlow()
}
}
.collect { groupId ->
try { try {
retryWithUniformInterval { retryWithUniformInterval {
pushGroupConfigsChangesIfNeeded(groupId) pushGroupConfigsChangesIfNeeded(groupId)

@ -56,7 +56,7 @@ object ResendMessageUtilities {
if (sentTimestamp != null && sender != null) { if (sentTimestamp != null && sender != null) {
if (isResync) { if (isResync) {
MessagingModuleConfiguration.shared.storage.markAsResyncing(sentTimestamp, sender) MessagingModuleConfiguration.shared.storage.markAsResyncing(sentTimestamp, sender)
MessageSender.send(message, Destination.from(recipient.address), isSyncMessage = true) MessageSender.sendNonDurably(message, Destination.from(recipient.address), isSyncMessage = true)
} else { } else {
MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender) MessagingModuleConfiguration.shared.storage.markAsSending(sentTimestamp, sender)
MessageSender.send(message, recipient.address) MessageSender.send(message, recipient.address)

@ -101,10 +101,38 @@ abstract class BaseGroupMembersViewModel (
status = status.takeIf { !isMyself }, // Status is only meant for other members status = status.takeIf { !isMyself }, // Status is only meant for other members
highlightStatus = highlightStatus, highlightStatus = highlightStatus,
showAsAdmin = member.isAdminOrBeingPromoted(status), showAsAdmin = member.isAdminOrBeingPromoted(status),
clickable = !isMyself clickable = !isMyself,
statusLabel = getMemberLabel(status, context, amIAdmin),
) )
} }
private fun getMemberLabel(status: GroupMember.Status, context: Context, amIAdmin: Boolean): String {
return when (status) {
GroupMember.Status.INVITE_FAILED -> context.getString(R.string.groupInviteFailed)
GroupMember.Status.INVITE_SENDING -> context.resources.getQuantityString(R.plurals.groupInviteSending, 1)
GroupMember.Status.INVITE_SENT -> context.getString(R.string.groupInviteSent)
GroupMember.Status.PROMOTION_FAILED -> context.getString(R.string.adminPromotionFailed)
GroupMember.Status.PROMOTION_SENDING -> context.resources.getQuantityString(R.plurals.adminSendingPromotion, 1)
GroupMember.Status.PROMOTION_SENT -> context.getString(R.string.adminPromotionSent)
GroupMember.Status.REMOVED,
GroupMember.Status.REMOVED_UNKNOWN,
GroupMember.Status.REMOVED_INCLUDING_MESSAGES -> {
if (amIAdmin) {
context.getString(R.string.groupPendingRemoval)
} else {
""
}
}
GroupMember.Status.INVITE_UNKNOWN,
GroupMember.Status.INVITE_ACCEPTED,
GroupMember.Status.INVITE_NOT_SENT,
GroupMember.Status.PROMOTION_NOT_SENT,
GroupMember.Status.PROMOTION_UNKNOWN,
GroupMember.Status.PROMOTION_ACCEPTED -> ""
}
}
// Refer to notion doc for the sorting logic // Refer to notion doc for the sorting logic
private fun sortMembers(members: List<GroupMemberState>, currentUserId: AccountId) = private fun sortMembers(members: List<GroupMemberState>, currentUserId: AccountId) =
members.sortedWith( members.sortedWith(
@ -150,37 +178,8 @@ data class GroupMemberState(
val canResendPromotion: Boolean, val canResendPromotion: Boolean,
val canRemove: Boolean, val canRemove: Boolean,
val canPromote: Boolean, val canPromote: Boolean,
val clickable: Boolean val clickable: Boolean,
val statusLabel: String,
) { ) {
val canEdit: Boolean get() = canRemove || canPromote || canResendInvite || canResendPromotion val canEdit: Boolean get() = canRemove || canPromote || canResendInvite || canResendPromotion
} }
// Function to get the label dynamically using the context
fun GroupMemberState.getLabel(context: Context): String {
return when (this.status) {
GroupMember.Status.INVITE_FAILED -> context.getString(R.string.groupInviteFailed)
GroupMember.Status.INVITE_SENDING -> context.resources.getQuantityString(R.plurals.groupInviteSending, 1)
GroupMember.Status.INVITE_SENT -> context.getString(R.string.groupInviteSent)
GroupMember.Status.PROMOTION_FAILED -> context.getString(R.string.adminPromotionFailed)
GroupMember.Status.PROMOTION_SENDING -> context.resources.getQuantityString(R.plurals.adminSendingPromotion, 1)
GroupMember.Status.PROMOTION_SENT -> context.getString(R.string.adminPromotionSent)
GroupMember.Status.REMOVED,
GroupMember.Status.REMOVED_UNKNOWN,
GroupMember.Status.REMOVED_INCLUDING_MESSAGES -> {
if (this.showAsAdmin) {
context.getString(R.string.groupPendingRemoval)
} else {
""
}
}
GroupMember.Status.INVITE_UNKNOWN,
GroupMember.Status.INVITE_ACCEPTED,
GroupMember.Status.INVITE_NOT_SENT,
GroupMember.Status.PROMOTION_NOT_SENT,
GroupMember.Status.PROMOTION_UNKNOWN,
GroupMember.Status.PROMOTION_ACCEPTED -> ""
null -> ""
}
}

@ -3,11 +3,9 @@ package org.thoughtcrime.securesms.groups
import android.content.Context import android.content.Context
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Deferred
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.awaitAll
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -19,7 +17,6 @@ import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
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
import network.loki.messenger.libsession_util.util.Sodium
import network.loki.messenger.libsession_util.util.UserPic import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
@ -269,8 +266,8 @@ class GroupManagerV2Impl @Inject constructor(
subAccountTokens = subAccountTokens subAccountTokens = subAccountTokens
) )
// Before we send the invitation, we need to make sure the configs are pushed // Send a group update message to the group telling members someone has been invited
configFactory.waitUntilGroupConfigsPushed(group) sendGroupUpdateForAddingMembers(group, adminKey, newMembers)
// Call the API // Call the API
try { try {
@ -307,9 +304,6 @@ class GroupManagerV2Impl @Inject constructor(
newMembers.map { it.hexString }.toTypedArray() newMembers.map { it.hexString }.toTypedArray()
) )
) )
// Send a group update message to the group telling members someone has been invited
sendGroupUpdateForAddingMembers(group, adminKey, newMembers)
} }
/** /**
@ -336,7 +330,8 @@ class GroupManagerV2Impl @Inject constructor(
) )
.build() .build()
).apply { this.sentTimestamp = timestamp } ).apply { this.sentTimestamp = timestamp }
MessageSender.send(updatedMessage, Destination.ClosedGroup(group.hexString), false)
MessageSender.send(updatedMessage, Address.fromSerialized(group.hexString))
storage.insertGroupInfoChange(updatedMessage, group) storage.insertGroupInfoChange(updatedMessage, group)
} }
@ -377,7 +372,7 @@ class GroupManagerV2Impl @Inject constructor(
updateMessage updateMessage
).apply { sentTimestamp = timestamp } ).apply { sentTimestamp = timestamp }
MessageSender.send(message, Destination.ClosedGroup(groupAccountId.hexString), false).await() MessageSender.send(message, Address.fromSerialized(groupAccountId.hexString))
storage.insertGroupInfoChange(message, groupAccountId) storage.insertGroupInfoChange(message, groupAccountId)
} }
@ -444,37 +439,27 @@ class GroupManagerV2Impl @Inject constructor(
} }
if (group != null && !group.kicked && !weAreTheOnlyAdmin) { if (group != null && !group.kicked && !weAreTheOnlyAdmin) {
val destination = Destination.ClosedGroup(groupId.hexString) val address = Address.fromSerialized(groupId.hexString)
val sendMessageTasks = mutableListOf<Deferred<*>>()
// Always send a "XXX left" message to the group if we can // Always send a "XXX left" message to the group if we can
sendMessageTasks += async { MessageSender.send(
MessageSender.send( GroupUpdated(
GroupUpdated( GroupUpdateMessage.newBuilder()
GroupUpdateMessage.newBuilder() .setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance())
.setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance()) .build()
.build() ),
), address
destination, )
isSyncMessage = false
).await()
}
// If we are not the only admin, send a left message for other admin to handle the member removal // If we are not the only admin, send a left message for other admin to handle the member removal
sendMessageTasks += async { MessageSender.send(
MessageSender.send( GroupUpdated(
GroupUpdated( GroupUpdateMessage.newBuilder()
GroupUpdateMessage.newBuilder() .setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance())
.setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance()) .build()
.build() ),
), address,
destination, )
isSyncMessage = false
).await()
}
sendMessageTasks.awaitAll()
} }
// If we are the only admin, leaving this group will destroy the group // If we are the only admin, leaving this group will destroy the group
@ -537,11 +522,10 @@ class GroupManagerV2Impl @Inject constructor(
val promotionDeferred = members.associateWith { member -> val promotionDeferred = members.associateWith { member ->
async { async {
MessageSender.sendNonDurably( MessageSender.sendAndAwait(
message = promoteMessage, message = promoteMessage,
address = Address.fromSerialized(member.hexString), address = Address.fromSerialized(member.hexString),
isSyncMessage = false )
).await()
} }
} }
@ -585,7 +569,7 @@ class GroupManagerV2Impl @Inject constructor(
sentTimestamp = timestamp sentTimestamp = timestamp
} }
MessageSender.send(message, Destination.ClosedGroup(group.hexString), false).await() MessageSender.sendAndAwait(message, Address.fromSerialized(group.hexString))
storage.insertGroupInfoChange(message, group) storage.insertGroupInfoChange(message, group)
} }
} }
@ -683,7 +667,7 @@ class GroupManagerV2Impl @Inject constructor(
.setInviteResponse(inviteResponse) .setInviteResponse(inviteResponse)
val responseMessage = GroupUpdated(responseData.build(), profile = storage.getUserProfile()) val responseMessage = GroupUpdated(responseData.build(), profile = storage.getUserProfile())
// this will fail the first couple of times :) // this will fail the first couple of times :)
MessageSender.send( MessageSender.sendNonDurably(
responseMessage, responseMessage,
Destination.ClosedGroup(group.groupAccountId.hexString), Destination.ClosedGroup(group.groupAccountId.hexString),
isSyncMessage = false isSyncMessage = false
@ -962,7 +946,7 @@ class GroupManagerV2Impl @Inject constructor(
sentTimestamp = timestamp sentTimestamp = timestamp
} }
MessageSender.send(message, Address.fromSerialized(groupId.hexString)) MessageSender.sendAndAwait(message, Address.fromSerialized(groupId.hexString))
storage.insertGroupInfoChange(message, groupId) storage.insertGroupInfoChange(message, groupId)
} }
@ -1034,7 +1018,7 @@ class GroupManagerV2Impl @Inject constructor(
sentTimestamp = timestamp sentTimestamp = timestamp
} }
MessageSender.send(message, Destination.ClosedGroup(groupId.hexString), false).await() MessageSender.sendAndAwait(message, Address.fromSerialized(groupId.hexString))
} }
override suspend fun handleDeleteMemberContent( override suspend fun handleDeleteMemberContent(

@ -50,7 +50,6 @@ import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.EditGroupViewModel import org.thoughtcrime.securesms.groups.EditGroupViewModel
import org.thoughtcrime.securesms.groups.GroupMemberState import org.thoughtcrime.securesms.groups.GroupMemberState
import org.thoughtcrime.securesms.groups.getLabel
import org.thoughtcrime.securesms.ui.AlertDialog import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.GetString
@ -442,7 +441,7 @@ fun EditMemberItem(
MemberItem( MemberItem(
accountId = member.accountId, accountId = member.accountId,
title = member.name, title = member.name,
subtitle = member.getLabel(LocalContext.current), subtitle = member.statusLabel,
subtitleColor = if (member.highlightStatus) { subtitleColor = if (member.highlightStatus) {
LocalColors.current.danger LocalColors.current.danger
} else { } else {
@ -476,7 +475,8 @@ private fun EditGroupPreview3() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = "Invited"
) )
val twoMember = GroupMemberState( val twoMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"),
@ -488,7 +488,8 @@ private fun EditGroupPreview3() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = true, showAsAdmin = true,
clickable = true clickable = true,
statusLabel = "Promotion failed"
) )
val threeMember = GroupMemberState( val threeMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"),
@ -500,7 +501,8 @@ private fun EditGroupPreview3() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = ""
) )
val (editingName, setEditingName) = remember { mutableStateOf<String?>(null) } val (editingName, setEditingName) = remember { mutableStateOf<String?>(null) }
@ -551,7 +553,8 @@ private fun EditGroupPreview() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = "Invited"
) )
val twoMember = GroupMemberState( val twoMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"),
@ -563,7 +566,8 @@ private fun EditGroupPreview() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = true, showAsAdmin = true,
clickable = true clickable = true,
statusLabel = "Promotion failed"
) )
val threeMember = GroupMemberState( val threeMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"),
@ -575,7 +579,8 @@ private fun EditGroupPreview() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = ""
) )
val (editingName, setEditingName) = remember { mutableStateOf<String?>(null) } val (editingName, setEditingName) = remember { mutableStateOf<String?>(null) }
@ -628,7 +633,8 @@ private fun EditGroupEditNamePreview(
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = "Invited"
) )
val twoMember = GroupMemberState( val twoMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"),
@ -640,7 +646,8 @@ private fun EditGroupEditNamePreview(
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = true, showAsAdmin = true,
clickable = true clickable = true,
statusLabel = "Promotion failed"
) )
val threeMember = GroupMemberState( val threeMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"),
@ -652,7 +659,8 @@ private fun EditGroupEditNamePreview(
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = ""
) )
EditGroup( EditGroup(

@ -9,7 +9,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -18,7 +17,6 @@ import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.GroupMemberState import org.thoughtcrime.securesms.groups.GroupMemberState
import org.thoughtcrime.securesms.groups.GroupMembersViewModel import org.thoughtcrime.securesms.groups.GroupMembersViewModel
import org.thoughtcrime.securesms.groups.getLabel
import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.PreviewTheme
@ -43,7 +41,7 @@ fun GroupMembersScreen(
@Composable @Composable
fun GroupMembers( fun GroupMembers(
onBack: () -> Unit, onBack: () -> Unit,
members: List<GroupMemberState> members: List<GroupMemberState>,
) { ) {
Scaffold( Scaffold(
@ -62,7 +60,7 @@ fun GroupMembers(
MemberItem( MemberItem(
accountId = member.accountId, accountId = member.accountId,
title = member.name, title = member.name,
subtitle = member.getLabel(LocalContext.current), subtitle = member.statusLabel,
subtitleColor = if (member.highlightStatus) { subtitleColor = if (member.highlightStatus) {
LocalColors.current.danger LocalColors.current.danger
} else { } else {
@ -91,7 +89,8 @@ private fun EditGroupPreview() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = "Invited"
) )
val twoMember = GroupMemberState( val twoMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1235"),
@ -103,7 +102,8 @@ private fun EditGroupPreview() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = true, showAsAdmin = true,
clickable = true clickable = true,
statusLabel = "Promotion failed"
) )
val threeMember = GroupMemberState( val threeMember = GroupMemberState(
accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"), accountId = AccountId("05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1236"),
@ -115,7 +115,8 @@ private fun EditGroupPreview() {
canResendInvite = false, canResendInvite = false,
canResendPromotion = false, canResendPromotion = false,
showAsAdmin = false, showAsAdmin = false,
clickable = true clickable = true,
statusLabel = ""
) )
GroupMembers( GroupMembers(

@ -416,7 +416,7 @@ class DefaultConversationRepository @Inject constructor(
) )
} else { } else {
val message = MessageRequestResponse(true) val message = MessageRequestResponse(true)
MessageSender.send( MessageSender.sendNonDurably(
message = message, message = message,
destination = Destination.from(recipient.address), destination = Destination.from(recipient.address),
isSyncMessage = recipient.isLocalNumber isSyncMessage = recipient.isLocalNumber

@ -139,7 +139,8 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
destination.whisperTo, destination.whisperTo,
destination.whisperMods, destination.whisperMods,
destination.fileIds + uploadResult.id.toString() destination.fileIds + uploadResult.id.toString()
) ),
statusCallback = it.statusCallback
) )
updatedJob.id = it.id updatedJob.id = it.id
updatedJob.delegate = it.delegate updatedJob.delegate = it.delegate

@ -7,7 +7,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.session.libsession.R
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.groups.GroupInviteException import org.session.libsession.messaging.groups.GroupInviteException
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
@ -18,11 +17,7 @@ import org.session.libsession.messaging.utilities.MessageAuthentication.buildGro
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.utilities.await import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.OTHER_NAME_KEY
import org.session.libsession.utilities.getGroup import org.session.libsession.utilities.getGroup
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage
import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
@ -81,7 +76,7 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array<
sentTimestamp = timestamp sentTimestamp = timestamp
} }
MessageSender.send(update, Destination.Contact(memberSessionId), false) MessageSender.sendNonDurably(update, Destination.Contact(memberSessionId), false)
.await() .await()
} }
} }

@ -3,10 +3,10 @@ package org.session.libsession.messaging.jobs
import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
@ -23,7 +23,7 @@ import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
class MessageSendJob(val message: Message, val destination: Destination) : Job { class MessageSendJob(val message: Message, val destination: Destination, val statusCallback: SendChannel<Result<Unit>>?) : Job {
object AwaitingAttachmentUploadException : Exception("Awaiting attachment upload.") object AwaitingAttachmentUploadException : Exception("Awaiting attachment upload.")
@ -91,18 +91,25 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
.waitForGroupEncryptionKeys(AccountId(destination.publicKey)) .waitForGroupEncryptionKeys(AccountId(destination.publicKey))
} }
MessageSender.send(this@MessageSendJob.message, destination, isSync).await() MessageSender.sendNonDurably(this@MessageSendJob.message, destination, isSync).await()
} }
this.handleSuccess(dispatcherName) this.handleSuccess(dispatcherName)
statusCallback?.trySend(Result.success(Unit))
} catch (e: HTTP.HTTPRequestFailedException) { } catch (e: HTTP.HTTPRequestFailedException) {
if (e.statusCode == 429) { this.handlePermanentFailure(dispatcherName, e) } if (e.statusCode == 429) { this.handlePermanentFailure(dispatcherName, e) }
else { this.handleFailure(dispatcherName, e) } else { this.handleFailure(dispatcherName, e) }
statusCallback?.trySend(Result.failure(e))
} catch (e: MessageSender.Error) { } catch (e: MessageSender.Error) {
if (!e.isRetryable) { this.handlePermanentFailure(dispatcherName, e) } if (!e.isRetryable) { this.handlePermanentFailure(dispatcherName, e) }
else { this.handleFailure(dispatcherName, e) } else { this.handleFailure(dispatcherName, e) }
statusCallback?.trySend(Result.failure(e))
} catch (e: Exception) { } catch (e: Exception) {
this.handleFailure(dispatcherName, e) this.handleFailure(dispatcherName, e)
statusCallback?.trySend(Result.failure(e))
} }
} }
@ -191,7 +198,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
} }
destinationInput.close() destinationInput.close()
// Return // Return
return MessageSendJob(message, destination) return MessageSendJob(message, destination, statusCallback = null)
} }
} }
} }

@ -2,9 +2,10 @@ package org.session.libsession.messaging.sending_receiving
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
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.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
@ -30,24 +31,20 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability
import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.messaging.open_groups.OpenGroupMessage
import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.RawResponsePromise
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.snode.SnodeAPI.nowWithOffset
import org.session.libsession.snode.SnodeMessage import org.session.libsession.snode.SnodeMessage
import org.session.libsession.snode.SnodeModule import org.session.libsession.snode.SnodeModule
import org.session.libsession.snode.utilities.asyncPromise import org.session.libsession.snode.utilities.asyncPromise
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Device import org.session.libsession.utilities.Device
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.PushTransportDetails import org.session.libsignal.crypto.PushTransportDetails
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.defaultRequiresAuth import org.session.libsignal.utilities.defaultRequiresAuth
import org.session.libsignal.utilities.hasNamespaces import org.session.libsignal.utilities.hasNamespaces
@ -80,7 +77,7 @@ object MessageSender {
} }
// Convenience // Convenience
fun send(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> { fun sendNonDurably(message: Message, destination: Destination, isSyncMessage: Boolean): Promise<Unit, Exception> {
if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, message.sentTimestamp!!) if (message is VisibleMessage) MessagingModuleConfiguration.shared.lastSentTimestampCache.submitTimestamp(message.threadID!!, message.sentTimestamp!!)
return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) {
sendToOpenGroupDestination(destination, message) sendToOpenGroupDestination(destination, message)
@ -548,12 +545,13 @@ object MessageSender {
} }
@JvmStatic @JvmStatic
fun send(message: Message, address: Address) { @JvmOverloads
fun send(message: Message, address: Address, statusCallback: SendChannel<Result<Unit>>? = null) {
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
threadID?.let(message::applyExpiryMode) threadID?.let(message::applyExpiryMode)
message.threadID = threadID message.threadID = threadID
val destination = Destination.from(address) val destination = Destination.from(address)
val job = MessageSendJob(message, destination) val job = MessageSendJob(message, destination, statusCallback)
JobQueue.shared.add(job) JobQueue.shared.add(job)
// if we are sending a 'Note to Self' make sure it is not hidden // if we are sending a 'Note to Self' make sure it is not hidden
@ -565,6 +563,12 @@ object MessageSender {
} }
} }
suspend fun sendAndAwait(message: Message, address: Address) {
val resultChannel = Channel<Result<Unit>>()
send(message, address, resultChannel)
resultChannel.receive().getOrThrow()
}
fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address, isSyncMessage: Boolean): Promise<Unit, Exception> { fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address, isSyncMessage: Boolean): Promise<Unit, Exception> {
val attachmentIDs = MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!) val attachmentIDs = MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!)
message.attachmentIDs.addAll(attachmentIDs) message.attachmentIDs.addAll(attachmentIDs)
@ -575,7 +579,7 @@ object MessageSender {
val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address)
message.threadID = threadID message.threadID = threadID
val destination = Destination.from(address) val destination = Destination.from(address)
return send(message, destination, isSyncMessage) return sendNonDurably(message, destination, isSyncMessage)
} }
// Closed groups // Closed groups

@ -6,11 +6,9 @@ import com.google.protobuf.ByteString
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.GroupLeavingJob import org.session.libsession.messaging.jobs.GroupLeavingJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1 import org.session.libsession.messaging.sending_receiving.notifications.PushRegistryV1
@ -22,14 +20,12 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.Device import org.session.libsession.utilities.Device
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.crypto.ecc.Curve import org.session.libsignal.crypto.ecc.Curve
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.protos.SignalServiceProtos import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.removingIdPrefixIfNeeded import org.session.libsignal.utilities.removingIdPrefixIfNeeded
@ -85,7 +81,8 @@ fun MessageSender.create(
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind, groupID) val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind, groupID)
closedGroupControlMessage.sentTimestamp = sentTime closedGroupControlMessage.sentTimestamp = sentTime
try { try {
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member), member == ourPubKey).await() sendNonDurably(closedGroupControlMessage, fromSerialized(member), member == ourPubKey)
.await()
} catch (e: Exception) { } catch (e: Exception) {
// We failed to properly create the group so delete it's associated data (in the past // We failed to properly create the group so delete it's associated data (in the past
// we didn't create this data until the messages successfully sent but this resulted // we didn't create this data until the messages successfully sent but this resulted

Loading…
Cancel
Save