Merge pull request #879 from session-foundation/merge-groups-to-dev

Merge latest changes to groups back to dev
pull/1710/head
SessionHero01 3 months ago committed by GitHub
commit 2a3db7d3f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import network.loki.messenger.libsession_util.allWithStatus
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsignal.utilities.AccountId
@ -114,7 +115,9 @@ class MentionViewModel(
}
} else if (recipient.isGroupV2Recipient) {
configFactory.withGroupConfigs(AccountId(recipient.address.serialize())) {
it.groupMembers.all().filterTo(hashSetOf()) { it.isAdminOrBeingPromoted }
it.groupMembers.allWithStatus()
.filter { (member, status) -> member.isAdminOrBeingPromoted(status) }
.mapTo(hashSetOf()) { (member, _) -> member.accountId.toString() }
}
} else {
emptySet()

@ -414,7 +414,7 @@ object ConversationMenuHelper {
doLeave = {
try {
channel.send(GroupLeavingStatus.Leaving)
groupManager.leaveGroup(accountId, true)
groupManager.leaveGroup(accountId)
channel.send(GroupLeavingStatus.Left)
} catch (e: Exception) {
channel.send(GroupLeavingStatus.Error)

@ -1131,7 +1131,7 @@ open class Storage @Inject constructor(
return groupDatabase.getAllGroups(includeInactive)
}
override fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? {
override suspend fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo? {
return OpenGroupManager.addOpenGroup(urlAsString, context)
}

@ -5,7 +5,6 @@ import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.ConfigBase
@ -49,7 +48,6 @@ import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.configs.ConfigToDatabaseSync
import org.thoughtcrime.securesms.database.ConfigDatabase

@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.AssistedFactory
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@ -17,6 +16,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.libsession_util.allWithStatus
import network.loki.messenger.libsession_util.util.GroupDisplayInfo
import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.database.StorageProtocol
@ -50,14 +50,16 @@ abstract class BaseGroupMembersViewModel (
val displayInfo = storage.getClosedGroupDisplayInfo(groupId.hexString)
?: return@withContext null
val memberState = storage.getMembers(groupId.hexString)
.map { member ->
val memberState = configFactory.withGroupConfigs(groupId) { it.groupMembers.allWithStatus() }
.map { (member, status) ->
createGroupMember(
member = member,
status = status,
myAccountId = currentUserId,
amIAdmin = displayInfo.isUserAdmin,
)
}
.toList()
displayInfo to sortMembers(memberState, currentUserId)
}
@ -70,6 +72,7 @@ abstract class BaseGroupMembersViewModel (
private fun createGroupMember(
member: GroupMember,
status: GroupMember.Status,
myAccountId: AccountId,
amIAdmin: Boolean,
): GroupMemberState {
@ -80,7 +83,7 @@ abstract class BaseGroupMembersViewModel (
member.getMemberName(configFactory)
}
val highlightStatus = member.status in EnumSet.of(
val highlightStatus = status in EnumSet.of(
GroupMember.Status.INVITE_FAILED,
GroupMember.Status.PROMOTION_FAILED
)
@ -89,17 +92,17 @@ abstract class BaseGroupMembersViewModel (
accountId = member.accountId,
name = name,
canRemove = amIAdmin && member.accountId != myAccountId
&& !member.isAdminOrBeingPromoted && !member.removed,
&& !member.isAdminOrBeingPromoted(status) && !member.isRemoved(status),
canPromote = amIAdmin && member.accountId != myAccountId
&& !member.isAdminOrBeingPromoted && !member.removed,
&& !member.isAdminOrBeingPromoted(status) && !member.isRemoved(status),
canResendPromotion = amIAdmin && member.accountId != myAccountId
&& member.status == GroupMember.Status.PROMOTION_FAILED && !member.removed,
&& status == GroupMember.Status.PROMOTION_FAILED && !member.isRemoved(status),
canResendInvite = amIAdmin && member.accountId != myAccountId
&& !member.removed
&& (member.status == GroupMember.Status.INVITE_SENT || member.status == GroupMember.Status.INVITE_FAILED),
status = member.status?.takeIf { !isMyself }, // Status is only meant for other members
&& !member.isRemoved(status)
&& (status == GroupMember.Status.INVITE_SENT || status == GroupMember.Status.INVITE_FAILED),
status = status.takeIf { !isMyself }, // Status is only meant for other members
highlightStatus = highlightStatus,
showAsAdmin = member.isAdminOrBeingPromoted,
showAsAdmin = member.isAdminOrBeingPromoted(status),
clickable = !isMyself
)
}
@ -147,10 +150,10 @@ data class GroupMemberState(
fun GroupMember.Status.getLabel(context: Context): String {
return when (this) {
GroupMember.Status.INVITE_FAILED -> context.getString(R.string.groupInviteFailed)
GroupMember.Status.INVITE_NOT_SENT -> context.resources.getQuantityString(R.plurals.groupInviteSending, 1)
GroupMember.Status.INVENT_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_NOT_SENT -> context.resources.getQuantityString(R.plurals.adminSendingPromotion, 1)
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,
@ -158,6 +161,8 @@ fun GroupMember.Status.getLabel(context: Context): String {
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 -> ""
}

@ -218,7 +218,8 @@ class GroupManagerV2Impl @Inject constructor(
for (newMember in newMembers) {
val toSet = configs.groupMembers.get(newMember.hexString)
?.also { existing ->
if (existing.status == GroupMember.Status.INVITE_FAILED || existing.status == GroupMember.Status.INVITE_SENT) {
val status = configs.groupMembers.status(existing)
if (status == GroupMember.Status.INVITE_FAILED || status == GroupMember.Status.INVITE_SENT) {
existing.setSupplement(shareHistory)
}
}
@ -264,6 +265,9 @@ class GroupManagerV2Impl @Inject constructor(
subAccountTokens = subAccountTokens
)
// Before we send the invitation, we need to make sure the configs are pushed
configFactory.waitUntilGroupConfigsPushed(group)
// Call the API
try {
val swarmNode = SnodeAPI.getSingleTargetSnode(group.hexString).await()
@ -414,71 +418,73 @@ class GroupManagerV2Impl @Inject constructor(
}
}
override suspend fun leaveGroup(groupId: AccountId, deleteOnLeave: Boolean) = withContext(dispatcher + SupervisorJob()) {
override suspend fun leaveGroup(groupId: AccountId) = withContext(dispatcher + SupervisorJob()) {
val group = configFactory.getGroup(groupId)
// Only send the left/left notification group message when we are not kicked and we are not the only admin (only admin has a special treatment)
val weAreTheOnlyAdmin = configFactory.withGroupConfigs(groupId) { config ->
val allMembers = config.groupMembers.all()
allMembers.count { it.admin } == 1 &&
allMembers.first { it.admin }.accountIdString() == storage.getUserPublicKey()
}
if (group != null && !group.kicked && !weAreTheOnlyAdmin) {
val destination = Destination.ClosedGroup(groupId.hexString)
val sendMessageTasks = mutableListOf<Deferred<*>>()
// Always send a "XXX left" message to the group if we can
sendMessageTasks += async {
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance())
.build()
),
destination,
isSyncMessage = false
).await()
if (group?.destroyed != true) {
// Only send the left/left notification group message when we are not kicked and we are not the only admin (only admin has a special treatment)
val weAreTheOnlyAdmin = configFactory.withGroupConfigs(groupId) { config ->
val allMembers = config.groupMembers.all()
allMembers.count { it.admin } == 1 &&
allMembers.first { it.admin }
.accountIdString() == storage.getUserPublicKey()
}
if (group != null && !group.kicked && !weAreTheOnlyAdmin) {
val destination = Destination.ClosedGroup(groupId.hexString)
val sendMessageTasks = mutableListOf<Deferred<*>>()
// Always send a "XXX left" message to the group if we can
sendMessageTasks += async {
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance())
.build()
),
destination,
isSyncMessage = false
).await()
}
// If we are not the only admin, send a left message for other admin to handle the member removal
sendMessageTasks += async {
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance())
.build()
),
destination,
isSyncMessage = false
).await()
}
sendMessageTasks.awaitAll()
}
// If we are not the only admin, send a left message for other admin to handle the member removal
sendMessageTasks += async {
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance())
.build()
),
destination,
isSyncMessage = false
).await()
}
// If we are the only admin, leaving this group will destroy the group
if (weAreTheOnlyAdmin) {
configFactory.withMutableGroupConfigs(groupId) { configs ->
configs.groupInfo.destroyGroup()
sendMessageTasks.awaitAll()
}
// Must wait until the config is pushed, otherwise if we go through the rest
// of the code it will destroy the conversation, destroying the necessary configs
// along the way, we won't be able to push the "destroyed" state anymore.
configFactory.waitUntilGroupConfigsPushed(groupId)
// If we are the only admin, leaving this group will destroy the group
if (weAreTheOnlyAdmin) {
configFactory.withMutableGroupConfigs(groupId) { configs ->
configs.groupInfo.destroyGroup()
}
// Must wait until the config is pushed, otherwise if we go through the rest
// of the code it will destroy the conversation, destroying the necessary configs
// along the way, we won't be able to push the "destroyed" state anymore.
configFactory.waitUntilGroupConfigsPushed(groupId)
}
}
pollerFactory.pollerFor(groupId)?.stop()
if (deleteOnLeave) {
storage.getThreadId(Address.fromSerialized(groupId.hexString))
?.let(storage::deleteConversation)
configFactory.removeGroup(groupId)
lokiAPIDatabase.clearLastMessageHashes(groupId.hexString)
lokiAPIDatabase.clearReceivedMessageHashValues(groupId.hexString)
}
// Delete conversation and group configs
storage.getThreadId(Address.fromSerialized(groupId.hexString))
?.let(storage::deleteConversation)
configFactory.removeGroup(groupId)
lokiAPIDatabase.clearLastMessageHashes(groupId.hexString)
lokiAPIDatabase.clearReceivedMessageHashValues(groupId.hexString)
}
override suspend fun promoteMember(

@ -14,6 +14,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -74,7 +75,7 @@ object OpenGroupManager {
fun getCommunitiesWriteAccessFlow() = _communityWriteAccess.asStateFlow()
@WorkerThread
fun add(server: String, room: String, publicKey: String, context: Context): Pair<Long,OpenGroupApi.RoomInfo?> {
suspend fun add(server: String, room: String, publicKey: String, context: Context): Pair<Long,OpenGroupApi.RoomInfo?> {
val openGroupID = "$server.$room"
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context)
val storage = MessagingModuleConfiguration.shared.storage
@ -90,7 +91,7 @@ object OpenGroupManager {
// Store the public key
storage.setOpenGroupPublicKey(server, publicKey)
// Get capabilities & room info
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get()
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).await()
storage.setServerCapabilities(server, capabilities.capabilities)
// Create the group locally if not available already
if (threadID < 0) {
@ -161,7 +162,7 @@ object OpenGroupManager {
}
@WorkerThread
fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
suspend fun addOpenGroup(urlAsString: String, context: Context): OpenGroupApi.RoomInfo? {
val url = urlAsString.toHttpUrlOrNull() ?: return null
val server = OpenGroup.getServer(urlAsString)
val room = url.pathSegments.firstOrNull() ?: return null

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.primaryOrange
@Composable
@ -42,7 +43,7 @@ fun GroupMinimumVersionBanner(modifier: Modifier = Modifier) {
maxLines = 2,
textAlign = TextAlign.Center,
modifier = Modifier
.background(LocalColors.current.warning)
.background(primaryOrange)
.fillMaxWidth()
.padding(
horizontal = LocalDimensions.current.spacing,

@ -28,6 +28,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@ -54,9 +55,10 @@ import org.thoughtcrime.securesms.groups.getLabel
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.LoadingDialog
import org.thoughtcrime.securesms.ui.components.ActionSheet
import org.thoughtcrime.securesms.ui.components.ActionSheetItemData
import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.components.annotatedStringResource
@ -102,6 +104,7 @@ fun EditGroupScreen(
onMemberClicked = viewModel::onMemberClicked,
hideActionSheet = viewModel::hideActionBottomSheet,
clickedMember = viewModel.clickedMember.collectAsState().value,
showLoading = viewModel.inProgress.collectAsState().value,
)
}
@ -122,7 +125,7 @@ fun EditGroupScreen(
@Serializable
private object RouteEditGroup
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun EditGroup(
onBack: () -> Unit,
@ -144,6 +147,7 @@ fun EditGroup(
members: List<GroupMemberState>,
showAddMembers: Boolean,
showingError: String?,
showLoading: Boolean,
onErrorDismissed: () -> Unit,
) {
val (showingConfirmRemovingMember, setShowingConfirmRemovingMember) = remember {
@ -160,43 +164,46 @@ fun EditGroup(
)
}
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
GroupMinimumVersionBanner()
// Group name title
Crossfade(editingName != null, label = "Editable group name") { showNameEditing ->
if (showNameEditing) {
GroupNameContainer {
IconButton(
modifier = Modifier.size(LocalDimensions.current.spacing),
onClick = onEditNameCancelClicked) {
Icon(
painter = painterResource(R.drawable.ic_x),
contentDescription = stringResource(R.string.AccessibilityId_cancel),
tint = LocalColors.current.text,
)
}
Box {
Column(modifier = Modifier.padding(paddingValues)) {
GroupMinimumVersionBanner()
// Group name title
Crossfade(editingName != null, label = "Editable group name") { showNameEditing ->
if (showNameEditing) {
GroupNameContainer {
IconButton(
modifier = Modifier.size(LocalDimensions.current.spacing),
onClick = onEditNameCancelClicked
) {
Icon(
painter = painterResource(R.drawable.ic_x),
contentDescription = stringResource(R.string.AccessibilityId_cancel),
tint = LocalColors.current.text,
)
}
SessionOutlinedTextField(
modifier = Modifier.widthIn(
min = LocalDimensions.current.mediumSpacing,
max = maxNameWidth
)
.qaTag(stringResource(R.string.AccessibilityId_groupName)),
text = editingName.orEmpty(),
onChange = onEditingNameValueChanged,
textStyle = LocalType.current.h8,
singleLine = true,
innerPadding = PaddingValues(
horizontal = LocalDimensions.current.spacing,
vertical = LocalDimensions.current.smallSpacing
SessionOutlinedTextField(
modifier = Modifier
.widthIn(
min = LocalDimensions.current.mediumSpacing,
max = maxNameWidth
)
.qaTag(stringResource(R.string.AccessibilityId_groupName)),
text = editingName.orEmpty(),
onChange = onEditingNameValueChanged,
textStyle = LocalType.current.h8,
singleLine = true,
innerPadding = PaddingValues(
horizontal = LocalDimensions.current.spacing,
vertical = LocalDimensions.current.smallSpacing
)
)
)
IconButton(
modifier = Modifier.size(LocalDimensions.current.spacing),
onClick = onEditNameConfirmed) {
onClick = onEditNameConfirmed
) {
Icon(
painter = painterResource(R.drawable.ic_check),
contentDescription = stringResource(R.string.AccessibilityId_confirm),
@ -206,16 +213,17 @@ fun EditGroup(
}
} else {
GroupNameContainer {
Spacer(modifier = Modifier.weight(1f))
Text(
text = groupName,
style = LocalType.current.h4,
textAlign = TextAlign.Center,
modifier = Modifier.widthIn(max = maxNameWidth)
.padding(vertical = LocalDimensions.current.smallSpacing),
)
} else {
GroupNameContainer {
Spacer(modifier = Modifier.weight(1f))
Text(
text = groupName,
style = LocalType.current.h4,
textAlign = TextAlign.Center,
modifier = Modifier
.widthIn(max = maxNameWidth)
.padding(vertical = LocalDimensions.current.smallSpacing),
)
Box(modifier = Modifier.weight(1f)) {
if (canEditName) {
@ -235,40 +243,41 @@ fun EditGroup(
}
}
// Header & Add member button
Row(
modifier = Modifier.padding(
horizontal = LocalDimensions.current.smallSpacing,
vertical = LocalDimensions.current.xxsSpacing
),
verticalAlignment = CenterVertically
) {
Text(
stringResource(R.string.groupMembers),
modifier = Modifier.weight(1f),
style = LocalType.current.large,
color = LocalColors.current.text
)
if (showAddMembers) {
PrimaryOutlineButton(
stringResource(R.string.membersInvite),
onClick = onAddMemberClick,
modifier = Modifier.qaTag(stringResource(R.string.AccessibilityId_membersInvite))
// Header & Add member button
Row(
modifier = Modifier.padding(
horizontal = LocalDimensions.current.smallSpacing,
vertical = LocalDimensions.current.xxsSpacing
),
verticalAlignment = CenterVertically
) {
Text(
stringResource(R.string.groupMembers),
modifier = Modifier.weight(1f),
style = LocalType.current.large,
color = LocalColors.current.text
)
if (showAddMembers) {
PrimaryOutlineButton(
stringResource(R.string.membersInvite),
onClick = onAddMemberClick,
modifier = Modifier.qaTag(stringResource(R.string.AccessibilityId_membersInvite))
)
}
}
}
// List of members
LazyColumn(modifier = Modifier) {
items(members) { member ->
// Each member's view
EditMemberItem(
modifier = Modifier.fillMaxWidth(),
member = member,
onClick = { onMemberClicked(member) }
)
// List of members
LazyColumn(modifier = Modifier) {
items(members) { member ->
// Each member's view
EditMemberItem(
modifier = Modifier.fillMaxWidth(),
member = member,
onClick = { onMemberClicked(member) }
)
}
}
}
}
@ -308,6 +317,10 @@ fun EditGroup(
)
}
if (showLoading) {
LoadingDialog()
}
val context = LocalContext.current
LaunchedEffect(showingError) {
@ -519,7 +532,8 @@ private fun EditGroupPreview3() {
onErrorDismissed = {},
onMemberClicked = {},
hideActionSheet = {},
clickedMember = null
clickedMember = null,
showLoading = true,
)
}
}
@ -593,7 +607,8 @@ private fun EditGroupPreview() {
onErrorDismissed = {},
onMemberClicked = {},
hideActionSheet = {},
clickedMember = null
clickedMember = null,
showLoading = false,
)
}
}
@ -661,7 +676,8 @@ private fun EditGroupEditNamePreview(
onErrorDismissed = {},
onMemberClicked = {},
hideActionSheet = {},
clickedMember = null
clickedMember = null,
showLoading = false,
)
}
}

@ -3,14 +3,12 @@ package org.thoughtcrime.securesms.groups.handler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getGroup
import org.session.libsignal.utilities.AccountId
import java.util.EnumSet
import javax.inject.Inject
@ -71,7 +69,7 @@ class AdminStateSync @Inject constructor(
private fun isMemberPromotionPending(groupId: AccountId, localNumber: String): Boolean {
return configFactory.withGroupConfigs(groupId) { groupConfigs ->
val status = groupConfigs.groupMembers.get(localNumber)?.status
val status = groupConfigs.groupMembers.get(localNumber)?.let(groupConfigs.groupMembers::status)
status != null && status in EnumSet.of(
GroupMember.Status.PROMOTION_SENT,
GroupMember.Status.PROMOTION_FAILED,

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.libsession_util.ReadableGroupKeysConfig
import network.loki.messenger.libsession_util.allWithStatus
import network.loki.messenger.libsession_util.util.GroupMember
import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.database.MessageDataProvider
@ -105,7 +106,10 @@ class RemoveGroupMemberHandler @Inject constructor(
val groupAuth = OwnedSwarmAuth.ofClosedGroup(groupAccountId, adminKey)
val (pendingRemovals, batchCalls) = configFactory.withGroupConfigs(groupAccountId) { configs ->
val pendingRemovals = configs.groupMembers.all().filter { it.removed }
val pendingRemovals = configs.groupMembers.allWithStatus()
.filter { (member, status) -> member.isRemoved(status) }
.toList()
if (pendingRemovals.isEmpty()) {
// Skip if there are no pending removals
return@withGroupConfigs pendingRemovals to emptyList()
@ -124,8 +128,8 @@ class RemoveGroupMemberHandler @Inject constructor(
calls += checkNotNull(
SnodeAPI.buildAuthenticatedRevokeSubKeyBatchRequest(
groupAdminAuth = groupAuth,
subAccountTokens = pendingRemovals.map {
configs.groupKeys.getSubAccountToken(it.accountId)
subAccountTokens = pendingRemovals.map { (member, _) ->
configs.groupKeys.getSubAccountToken(member.accountId)
}
)
) { "Fail to create a revoke request" }
@ -135,7 +139,7 @@ class RemoveGroupMemberHandler @Inject constructor(
namespace = Namespace.REVOKED_GROUP_MESSAGES(),
message = buildGroupKickMessage(
groupAccountId.hexString,
pendingRemovals,
pendingRemovals.map { it.first },
configs.groupKeys,
adminKey
),
@ -143,7 +147,7 @@ class RemoveGroupMemberHandler @Inject constructor(
)
// Call No 3. Conditionally send the `GroupUpdateDeleteMemberContent`
if (pendingRemovals.any { it.shouldRemoveMessages }) {
if (pendingRemovals.any { (member, status) -> member.shouldRemoveMessages(status) }) {
calls += SnodeAPI.buildAuthenticatedStoreBatchInfo(
namespace = Namespace.CLOSED_GROUP_MESSAGES(),
message = buildDeleteGroupMemberContentMessage(
@ -151,8 +155,8 @@ class RemoveGroupMemberHandler @Inject constructor(
groupAccountId = groupAccountId.hexString,
memberSessionIDs = pendingRemovals
.asSequence()
.filter { it.shouldRemoveMessages }
.map { it.accountIdString() },
.filter { (member, status) -> member.shouldRemoveMessages(status) }
.map { (member, _) -> member.accountIdString() },
),
auth = groupAuth,
)
@ -179,8 +183,8 @@ class RemoveGroupMemberHandler @Inject constructor(
// The essential part of the operation has been successful once we get to this point,
// now we can go ahead and update the configs
configFactory.withMutableGroupConfigs(groupAccountId) { configs ->
pendingRemovals.forEach {
configs.groupMembers.erase(it.accountIdString())
pendingRemovals.forEach { (member, _) ->
configs.groupMembers.erase(member.accountIdString())
}
configs.rekey()
}
@ -191,12 +195,12 @@ class RemoveGroupMemberHandler @Inject constructor(
// Try to delete members' message. It's ok to fail as they will be re-tried in different
// cases (a.k.a the GroupUpdateDeleteMemberContent message handling) and could be by different admins.
val deletingMessagesForMembers = pendingRemovals.filter { it.shouldRemoveMessages }
val deletingMessagesForMembers = pendingRemovals.filter { (member, status) -> member.shouldRemoveMessages(status) }
if (deletingMessagesForMembers.isNotEmpty()) {
val threadId = storage.getThreadId(Address.fromSerialized(groupAccountId.hexString))
if (threadId != null) {
val until = clock.currentTimeMills()
for (member in deletingMessagesForMembers) {
for ((member, _) in deletingMessagesForMembers) {
try {
messageDataProvider.markUserMessagesAsDeleted(
threadId = threadId,

@ -20,6 +20,7 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.database.userAuth
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.utilities.await
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.createSessionDialog
@ -145,9 +146,9 @@ class ClearAllDataDialog : DialogFragment() {
val deletionResultMap: Map<String, Boolean>? = try {
val openGroups = DatabaseComponent.get(requireContext()).lokiThreadDatabase().getAllOpenGroups()
openGroups.map { it.value.server }.toSet().forEach { server ->
OpenGroupApi.deleteAllInboxMessages(server).get()
OpenGroupApi.deleteAllInboxMessages(server).await()
}
SnodeAPI.deleteAllMessages(checkNotNull(storage.userAuth)).get()
SnodeAPI.deleteAllMessages(checkNotNull(storage.userAuth)).await()
} catch (e: Exception) {
Log.e(TAG, "Failed to delete network messages - offering user option to delete local data only.", e)
null

@ -128,7 +128,8 @@ fun AlertDialog(
text = it,
textAlign = TextAlign.Center,
style = LocalType.current.h7,
modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
modifier = Modifier
.padding(bottom = LocalDimensions.current.xxsSpacing)
.qaTag(stringResource(R.string.AccessibilityId_modalTitle))
)
}
@ -281,22 +282,33 @@ fun LoadingDialog(
modifier = modifier,
onDismissRequest = {},
content = {
DialogBg {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(LocalDimensions.current.spacing)
) {
if (title.isNullOrBlank()) {
Box {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
modifier = Modifier.align(Alignment.Center),
//TODO: Leave this as hardcoded color for now as the dialog background (scrim)
// always seems to be dark. Can can revisit later when we have more control over
// the scrim color.
color = Color.White
)
}
} else {
DialogBg {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(LocalDimensions.current.spacing)
) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(LocalDimensions.current.spacing))
Spacer(modifier = Modifier.height(LocalDimensions.current.spacing))
title?.let {
Text(
it,
modifier = Modifier.align(Alignment.CenterHorizontally)
title,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.qaTag(stringResource(R.string.AccessibilityId_modalTitle)),
style = LocalType.current.large
)

@ -1 +1 @@
Subproject commit 43b1c6c341ee8739a8678c631d0713136dbfd05f
Subproject commit b1bd153a4ef7214f60e3a5f4ce7d939e6ac22024

@ -21,33 +21,35 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_00024Companion_newI
jbyteArray initial_dump,
jlong info_pointer,
jlong members_pointer) {
std::lock_guard lock{util::util_mutex_};
auto user_key_bytes = util::ustring_from_bytes(env, user_secret_key);
auto pub_key_bytes = util::ustring_from_bytes(env, group_public_key);
std::optional<session::ustring> secret_key_optional{std::nullopt};
std::optional<session::ustring> initial_dump_optional{std::nullopt};
return jni_utils::run_catching_cxx_exception_or_throws<jlong>(env, [=] {
std::lock_guard lock{util::util_mutex_};
auto user_key_bytes = util::ustring_from_bytes(env, user_secret_key);
auto pub_key_bytes = util::ustring_from_bytes(env, group_public_key);
std::optional<session::ustring> secret_key_optional{std::nullopt};
std::optional<session::ustring> initial_dump_optional{std::nullopt};
if (group_secret_key && env->GetArrayLength(group_secret_key) > 0) {
auto secret_key_bytes = util::ustring_from_bytes(env, group_secret_key);
secret_key_optional = secret_key_bytes;
}
if (group_secret_key && env->GetArrayLength(group_secret_key) > 0) {
auto secret_key_bytes = util::ustring_from_bytes(env, group_secret_key);
secret_key_optional = secret_key_bytes;
}
if (initial_dump && env->GetArrayLength(initial_dump) > 0) {
auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump);
initial_dump_optional = initial_dump_bytes;
}
if (initial_dump && env->GetArrayLength(initial_dump) > 0) {
auto initial_dump_bytes = util::ustring_from_bytes(env, initial_dump);
initial_dump_optional = initial_dump_bytes;
}
auto info = reinterpret_cast<session::config::groups::Info*>(info_pointer);
auto members = reinterpret_cast<session::config::groups::Members*>(members_pointer);
auto info = reinterpret_cast<session::config::groups::Info*>(info_pointer);
auto members = reinterpret_cast<session::config::groups::Members*>(members_pointer);
auto* keys = new session::config::groups::Keys(user_key_bytes,
pub_key_bytes,
secret_key_optional,
initial_dump_optional,
*info,
*members);
auto* keys = new session::config::groups::Keys(user_key_bytes,
pub_key_bytes,
secret_key_optional,
initial_dump_optional,
*info,
*members);
return reinterpret_cast<jlong>(keys);
return reinterpret_cast<jlong>(keys);
});
}
extern "C"

@ -149,12 +149,6 @@ Java_network_loki_messenger_libsession_1util_util_GroupMember_setRemoved(JNIEnv
ptrToMember(env, thiz)->set_removed(also_remove_messages);
}
extern "C"
JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_util_GroupMember_statusInt(JNIEnv *env, jobject thiz) {
return static_cast<jint>(ptrToMember(env, thiz)->status());
}
extern "C"
JNIEXPORT void JNICALL
Java_network_loki_messenger_libsession_1util_util_GroupMember_setName(JNIEnv *env, jobject thiz,
@ -225,3 +219,18 @@ Java_network_loki_messenger_libsession_1util_util_GroupMember_setSupplement(JNIE
ptrToMember(env, thiz)->supplement = supplement;
}
extern "C"
JNIEXPORT jint JNICALL
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_statusInt(JNIEnv *env, jobject thiz,
jobject group_member) {
return static_cast<jint>(ptrToMembers(env, thiz)->get_status(*ptrToMember(env, group_member)));
}
extern "C"
JNIEXPORT void JNICALL
Java_network_loki_messenger_libsession_1util_GroupMembersConfig_setPendingSend(JNIEnv *env,
jobject thiz,
jstring pub_key_hex,
jboolean pending) {
ptrToMembers(env, thiz)->set_pending_send(util::string_from_jstring(env, pub_key_hex), pending);
}

@ -384,12 +384,19 @@ class GroupInfoConfig private constructor(pointer: Long): ConfigBase(pointer), M
interface ReadableGroupMembersConfig: ReadableConfig {
fun all(): List<GroupMember>
fun get(pubKeyHex: String): GroupMember?
fun status(groupMember: GroupMember): GroupMember.Status
}
fun ReadableGroupMembersConfig.allWithStatus(): Sequence<Pair<GroupMember, GroupMember.Status>> {
return all().asSequence().map { it to status(it) }
}
interface MutableGroupMembersConfig : ReadableGroupMembersConfig, MutableConfig {
fun getOrConstruct(pubKeyHex: String): GroupMember
fun set(groupMember: GroupMember)
fun erase(pubKeyHex: String): Boolean
fun setPendingSend(pubKeyHex: String, pending: Boolean)
}
class GroupMembersConfig private constructor(pointer: Long): ConfigBase(pointer), MutableGroupMembersConfig {
@ -411,6 +418,13 @@ class GroupMembersConfig private constructor(pointer: Long): ConfigBase(pointer)
external override fun get(pubKeyHex: String): GroupMember?
external override fun getOrConstruct(pubKeyHex: String): GroupMember
external override fun set(groupMember: GroupMember)
external override fun setPendingSend(pubKeyHex: String, pending: Boolean)
private external fun statusInt(groupMember: GroupMember): Int
override fun status(groupMember: GroupMember): GroupMember.Status {
val statusInt = statusInt(groupMember)
return GroupMember.Status.entries.first { it.nativeValue == statusInt }
}
}
sealed class ConfigSig(pointer: Long) : Config(pointer)

@ -31,9 +31,6 @@ class GroupMember private constructor(
external fun setRemoved(alsoRemoveMessages: Boolean)
private external fun statusInt(): Int
val status: Status? get() = Status.entries.firstOrNull { it.nativeValue == statusInt() }
external fun profilePic(): UserPic?
external fun setProfilePic(pic: UserPic)
@ -60,37 +57,43 @@ class GroupMember private constructor(
destroy()
}
val removed: Boolean
get() = status in EnumSet.of(Status.REMOVED, Status.REMOVED_UNKNOWN, Status.REMOVED_INCLUDING_MESSAGES)
fun isRemoved(status: Status): Boolean {
return status in EnumSet.of(Status.REMOVED, Status.REMOVED_UNKNOWN, Status.REMOVED_INCLUDING_MESSAGES)
}
val isAdminOrBeingPromoted: Boolean
get() = admin || status in EnumSet.of(Status.PROMOTION_SENT, Status.PROMOTION_ACCEPTED)
fun isAdminOrBeingPromoted(status: Status): Boolean {
return admin || status in EnumSet.of(Status.PROMOTION_SENT, Status.PROMOTION_ACCEPTED)
}
val inviteFailed: Boolean
get() = status == Status.INVITE_FAILED
fun inviteFailed(status: Status): Boolean {
return status == Status.INVITE_FAILED
}
val shouldRemoveMessages: Boolean
get() = status == Status.REMOVED_INCLUDING_MESSAGES
fun shouldRemoveMessages(status: Status): Boolean {
return status == Status.REMOVED_INCLUDING_MESSAGES
}
enum class Status(val nativeValue: Int) {
INVITE_UNKNOWN(0),
INVITE_NOT_SENT(1),
INVITE_FAILED(2),
INVITE_SENT(3),
INVITE_ACCEPTED(4),
PROMOTION_UNKNOWN(5),
PROMOTION_NOT_SENT(6),
PROMOTION_FAILED(7),
PROMOTION_SENT(8),
PROMOTION_ACCEPTED(9),
REMOVED_UNKNOWN(10),
REMOVED(11),
REMOVED_INCLUDING_MESSAGES(12);
INVENT_SENDING(2),
INVITE_FAILED(3),
INVITE_SENT(4),
INVITE_ACCEPTED(5),
PROMOTION_UNKNOWN(6),
PROMOTION_NOT_SENT(7),
PROMOTION_SENDING(8),
PROMOTION_FAILED(9),
PROMOTION_SENT(10),
PROMOTION_ACCEPTED(11),
REMOVED_UNKNOWN(12),
REMOVED(13),
REMOVED_INCLUDING_MESSAGES(14);
}
override fun toString(): String {
return "GroupMember(name=$name, admin=$admin, supplement=$supplement, status=$status)"
return "GroupMember(name=$name, admin=$admin, supplement=$supplement)"
}
}

@ -80,7 +80,7 @@ interface StorageProtocol {
fun getAllOpenGroups(): Map<Long, OpenGroup>
fun updateOpenGroup(openGroup: OpenGroup)
fun getOpenGroup(threadId: Long): OpenGroup?
fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo?
suspend fun addOpenGroup(urlAsString: String): OpenGroupApi.RoomInfo?
fun onOpenGroupAdded(server: String, room: String)
fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean)

@ -43,10 +43,7 @@ interface GroupManagerV2 {
)
suspend fun handleMemberLeftMessage(memberId: AccountId, group: AccountId)
suspend fun leaveGroup(groupId:
AccountId, deleteOnLeave: Boolean)
suspend fun leaveGroup(groupId: AccountId)
suspend fun promoteMember(group: AccountId, members: List<AccountId>)
suspend fun handleInvitation(

@ -4,6 +4,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.GroupUtil
class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: String?) : Job {
@ -32,7 +33,7 @@ class GroupAvatarDownloadJob(val server: String, val room: String, val imageId:
}
try {
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).await()
// Once the download is complete the imageId might no longer match, so we need to fetch it again just in case
val postDownloadStoredImageId = storage.getOpenGroup(room, server)?.imageId

@ -8,6 +8,7 @@ import kotlinx.coroutines.launch
import okio.Buffer
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLastProfilePictureUpload
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
@ -102,7 +103,7 @@ object ProfilePictureUtilities {
// this can throw an error
id = retryIfNeeded(4) {
FileServerApi.upload(data)
}.get()
}.await()
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
val url = "${FileServerApi.server}/file/$id"

Loading…
Cancel
Save