This commit is contained in:
SessionHero01 2024-10-21 11:55:27 +11:00
parent 0657ab2305
commit 88df9ff65a
No known key found for this signature in database
10 changed files with 136 additions and 122 deletions

View File

@ -196,12 +196,14 @@ class ConfigToDatabaseSync @Inject constructor(
private data class UpdateGroupInfo( private data class UpdateGroupInfo(
val id: AccountId, val id: AccountId,
val name: String?, val name: String?,
val destroyed: Boolean,
val deleteBefore: Long?, val deleteBefore: Long?,
val deleteAttachmentsBefore: Long? val deleteAttachmentsBefore: Long?
) { ) {
constructor(groupInfoConfig: ReadableGroupInfoConfig) : this( constructor(groupInfoConfig: ReadableGroupInfoConfig) : this(
id = groupInfoConfig.id(), id = groupInfoConfig.id(),
name = groupInfoConfig.getName(), name = groupInfoConfig.getName(),
destroyed = groupInfoConfig.isDestroyed(),
deleteBefore = groupInfoConfig.getDeleteBefore(), deleteBefore = groupInfoConfig.getDeleteBefore(),
deleteAttachmentsBefore = groupInfoConfig.getDeleteAttachmentsBefore() deleteAttachmentsBefore = groupInfoConfig.getDeleteAttachmentsBefore()
) )
@ -212,6 +214,10 @@ class ConfigToDatabaseSync @Inject constructor(
val recipient = storage.getRecipientForThread(threadId) ?: return val recipient = storage.getRecipientForThread(threadId) ?: return
recipientDatabase.setProfileName(recipient, groupInfoConfig.name) recipientDatabase.setProfileName(recipient, groupInfoConfig.name)
profileManager.setName(context, recipient, groupInfoConfig.name ?: "") profileManager.setName(context, recipient, groupInfoConfig.name ?: "")
if (groupInfoConfig.destroyed) {
storage.clearMessages(threadId)
} else {
groupInfoConfig.deleteBefore?.let { removeBefore -> groupInfoConfig.deleteBefore?.let { removeBefore ->
storage.trimThreadBefore(threadId, removeBefore) storage.trimThreadBefore(threadId, removeBefore)
} }
@ -219,6 +225,7 @@ class ConfigToDatabaseSync @Inject constructor(
mmsDatabase.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true) mmsDatabase.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true)
} }
} }
}
private data class UpdateContacts(val contacts: List<Contact>) private data class UpdateContacts(val contacts: List<Contact>)

View File

@ -1231,17 +1231,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (item.itemId == android.R.id.home) { if (item.itemId == android.R.id.home) {
return false return false
} }
return viewModel.recipient?.let { recipient ->
ConversationMenuHelper.onOptionItemSelected( return viewModel.onOptionItemSelected(this, item)
context = this,
item = item,
thread = recipient,
threadID = threadId,
factory = configFactory,
storage = storage,
groupManager = groupManagerV2,
)
} ?: false
} }
override fun block(deleteThread: Boolean) { override fun block(deleteThread: Boolean) {

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.view.MenuItem
import androidx.annotation.StringRes import androidx.annotation.StringRes
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -13,11 +14,8 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -25,6 +23,7 @@ import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.GroupMember import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupApi
@ -40,12 +39,14 @@ import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.AudioSlide import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
@ -61,7 +62,9 @@ class ConversationViewModel(
private val groupDb: GroupDatabase, private val groupDb: GroupDatabase,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val lokiMessageDb: LokiMessageDatabase, private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val groupManagerV2: GroupManagerV2,
) : ViewModel() { ) : ViewModel() {
val showSendAfterApprovalText: Boolean val showSendAfterApprovalText: Boolean
@ -225,7 +228,7 @@ class ConversationViewModel(
*/ */
private fun shouldShowInput(recipient: Recipient?): Boolean { private fun shouldShowInput(recipient: Recipient?): Boolean {
return when { return when {
recipient?.isClosedGroupV2Recipient == true -> !repository.isKicked(recipient) recipient?.isClosedGroupV2Recipient == true -> !repository.isGroupReadOnly(recipient)
recipient?.isLegacyClosedGroupRecipient == true -> { recipient?.isLegacyClosedGroupRecipient == true -> {
groupDb.getGroup(recipient.address.toGroupString()).orNull()?.isActive == true groupDb.getGroup(recipient.address.toGroupString()).orNull()?.isActive == true
} }
@ -872,6 +875,37 @@ class ConversationViewModel(
} }
} }
fun onOptionItemSelected(
// This must be the context of the activity as requirement from ConversationMenuHelper
context: Context,
item: MenuItem
): Boolean {
val recipient = recipient ?: return false
val inProgress = ConversationMenuHelper.onOptionItemSelected(
context = context,
item = item,
thread = recipient,
threadID = threadId,
factory = configFactory,
storage = storage,
groupManager = groupManagerV2,
)
if (inProgress != null) {
viewModelScope.launch {
_uiState.update { it.copy(showLoader = true) }
try {
inProgress.receive()
} finally {
_uiState.update { it.copy(showLoader = false) }
}
}
}
return true
}
@dagger.assisted.AssistedFactory @dagger.assisted.AssistedFactory
interface AssistedFactory { interface AssistedFactory {
fun create(threadId: Long, edKeyPair: KeyPair?): Factory fun create(threadId: Long, edKeyPair: KeyPair?): Factory
@ -890,7 +924,9 @@ class ConversationViewModel(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val lokiMessageDb: LokiMessageDatabase, private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val groupManagerV2: GroupManagerV2,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -904,7 +940,9 @@ class ConversationViewModel(
groupDb = groupDb, groupDb = groupDb,
threadDb = threadDb, threadDb = threadDb,
lokiMessageDb = lokiMessageDb, lokiMessageDb = lokiMessageDb,
textSecurePreferences = textSecurePreferences textSecurePreferences = textSecurePreferences,
configFactory = configFactory,
groupManagerV2 = groupManagerV2,
) as T ) as T
} }
} }

View File

@ -20,6 +20,8 @@ import androidx.core.graphics.drawable.IconCompat
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
@ -156,6 +158,12 @@ object ConversationMenuHelper {
}) })
} }
/**
* Handle the selected option
*
* @return An asynchronous channel that can be used to wait for the action to complete. Null if
* the action does not require waiting.
*/
fun onOptionItemSelected( fun onOptionItemSelected(
context: Context, context: Context,
item: MenuItem, item: MenuItem,
@ -164,7 +172,7 @@ object ConversationMenuHelper {
factory: ConfigFactory, factory: ConfigFactory,
storage: StorageProtocol, storage: StorageProtocol,
groupManager: GroupManagerV2, groupManager: GroupManagerV2,
): Boolean { ): ReceiveChannel<Unit>? {
when (item.itemId) { when (item.itemId) {
R.id.menu_view_all_media -> { showAllMedia(context, thread) } R.id.menu_view_all_media -> { showAllMedia(context, thread) }
R.id.menu_search -> { search(context) } R.id.menu_search -> { search(context) }
@ -176,14 +184,15 @@ object ConversationMenuHelper {
R.id.menu_copy_account_id -> { copyAccountID(context, thread) } R.id.menu_copy_account_id -> { copyAccountID(context, thread) }
R.id.menu_copy_open_group_url -> { copyOpenGroupUrl(context, thread) } R.id.menu_copy_open_group_url -> { copyOpenGroupUrl(context, thread) }
R.id.menu_edit_group -> { editClosedGroup(context, thread) } R.id.menu_edit_group -> { editClosedGroup(context, thread) }
R.id.menu_leave_group -> { leaveClosedGroup(context, thread, threadID, factory, storage, groupManager) } R.id.menu_leave_group -> { return leaveClosedGroup(context, thread, threadID, factory, storage, groupManager) }
R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) } R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) }
R.id.menu_unmute_notifications -> { unmute(context, thread) } R.id.menu_unmute_notifications -> { unmute(context, thread) }
R.id.menu_mute_notifications -> { mute(context, thread) } R.id.menu_mute_notifications -> { mute(context, thread) }
R.id.menu_notification_settings -> { setNotifyType(context, thread) } R.id.menu_notification_settings -> { setNotifyType(context, thread) }
R.id.menu_call -> { call(context, thread) } R.id.menu_call -> { call(context, thread) }
} }
return true
return null
} }
private fun showAllMedia(context: Context, thread: Recipient) { private fun showAllMedia(context: Context, thread: Recipient) {
@ -330,7 +339,7 @@ object ConversationMenuHelper {
configFactory: ConfigFactory, configFactory: ConfigFactory,
storage: StorageProtocol, storage: StorageProtocol,
groupManager: GroupManagerV2, groupManager: GroupManagerV2,
) { ): ReceiveChannel<Unit>? {
when { when {
thread.isLegacyClosedGroupRecipient -> { thread.isLegacyClosedGroupRecipient -> {
val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull() val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
@ -357,11 +366,13 @@ object ConversationMenuHelper {
thread.isClosedGroupV2Recipient -> { thread.isClosedGroupV2Recipient -> {
val accountId = AccountId(thread.address.serialize()) val accountId = AccountId(thread.address.serialize())
val group = configFactory.withUserConfigs { it.userGroups.getClosedGroup(accountId.hexString) } ?: return val group = configFactory.withUserConfigs { it.userGroups.getClosedGroup(accountId.hexString) } ?: return null
val name = configFactory.withGroupConfigs(accountId) { val name = configFactory.withGroupConfigs(accountId) {
it.groupInfo.getName() it.groupInfo.getName()
} ?: group.name } ?: group.name
val channel = Channel<Unit>()
confirmAndLeaveClosedGroup( confirmAndLeaveClosedGroup(
context = context, context = context,
groupName = name, groupName = name,
@ -369,11 +380,19 @@ object ConversationMenuHelper {
threadID = threadID, threadID = threadID,
storage = storage, storage = storage,
doLeave = { doLeave = {
try {
groupManager.leaveGroup(accountId, true) groupManager.leaveGroup(accountId, true)
} finally {
channel.send(Unit)
}
} }
) )
return channel
} }
} }
return null
} }
private fun confirmAndLeaveClosedGroup( private fun confirmAndLeaveClosedGroup(

View File

@ -365,7 +365,9 @@ class ConfigFactory @Inject constructor(
} }
} }
Unit to configs.dumpIfNeeded(clock) configs.dumpIfNeeded(clock)
Unit to true
} }
} }

View File

@ -388,12 +388,32 @@ class GroupManagerV2Impl @Inject constructor(
} }
} }
override suspend fun leaveGroup(group: AccountId, deleteOnLeave: Boolean) { override suspend fun leaveGroup(groupId: AccountId, deleteOnLeave: Boolean) {
val canSendGroupMessage = configFactory.getClosedGroup(group)?.kicked == false val group = configFactory.getClosedGroup(groupId)
if (canSendGroupMessage) { // 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 destination = Destination.ClosedGroup(group.hexString) val weAreTheOnlyAdmin = configFactory.withGroupConfigs(groupId) { config ->
val allMembers = config.groupMembers.all()
allMembers.count { it.admin } == 1 &&
allMembers.first { it.admin }.sessionId == storage.getUserPublicKey()
}
if (group?.kicked == false) {
val destination = Destination.ClosedGroup(groupId.hexString)
// Always send a "XXX left" message to the group if we can
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance())
.build()
),
destination,
isSyncMessage = false
)
// If we are not the only admin, send a left message for other admin to handle the member removal
if (!weAreTheOnlyAdmin) {
MessageSender.send( MessageSender.send(
GroupUpdated( GroupUpdated(
GroupUpdateMessage.newBuilder() GroupUpdateMessage.newBuilder()
@ -403,23 +423,27 @@ class GroupManagerV2Impl @Inject constructor(
destination, destination,
isSyncMessage = false isSyncMessage = false
).await() ).await()
}
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftNotificationMessage(DataMessage.GroupUpdateMemberLeftNotificationMessage.getDefaultInstance())
.build()
),
destination,
isSyncMessage = false
).await()
} }
pollerFactory.pollerFor(group)?.stop() // 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) { if (deleteOnLeave) {
storage.getThreadId(Address.fromSerialized(group.hexString)) storage.getThreadId(Address.fromSerialized(groupId.hexString))
?.let(storage::deleteConversation) ?.let(storage::deleteConversation)
configFactory.removeGroup(group) configFactory.removeGroup(groupId)
} }
} }

View File

@ -28,6 +28,7 @@ import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getClosedGroup
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseContentProviders
@ -59,7 +60,7 @@ interface ConversationRepository {
fun deleteMessages(messages: Set<MessageRecord>, threadId: Long) fun deleteMessages(messages: Set<MessageRecord>, threadId: Long)
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
fun setApproved(recipient: Recipient, isApproved: Boolean) fun setApproved(recipient: Recipient, isApproved: Boolean)
fun isKicked(recipient: Recipient): Boolean fun isGroupReadOnly(recipient: Recipient): Boolean
suspend fun deleteCommunityMessagesRemotely(threadId: Long, messages: Set<MessageRecord>) suspend fun deleteCommunityMessagesRemotely(threadId: Long, messages: Set<MessageRecord>)
suspend fun delete1on1MessagesRemotely( suspend fun delete1on1MessagesRemotely(
@ -170,15 +171,16 @@ class DefaultConversationRepository @Inject constructor(
} }
} }
override fun isKicked(recipient: Recipient): Boolean { override fun isGroupReadOnly(recipient: Recipient): Boolean {
// For now, we only know care we are kicked for a groups v2 recipient // We only care about group v2 recipient
if (!recipient.isClosedGroupV2Recipient) { if (!recipient.isClosedGroupV2Recipient) {
return false return false
} }
val groupId = recipient.address.serialize()
return configFactory.withUserConfigs { return configFactory.withUserConfigs {
it.userGroups.getClosedGroup(recipient.address.serialize())?.kicked == true it.userGroups.getClosedGroup(groupId)?.kicked == true
} } || configFactory.withGroupConfigs(AccountId(groupId)) { it.groupInfo.isDestroyed() }
} }
// This assumes that recipient.isContactRecipient is true // This assumes that recipient.isContactRecipient is true

View File

@ -44,7 +44,7 @@ interface GroupManagerV2 {
suspend fun handleMemberLeft(message: GroupUpdated, group: AccountId) suspend fun handleMemberLeft(message: GroupUpdated, group: AccountId)
suspend fun leaveGroup(group: AccountId, deleteOnLeave: Boolean) suspend fun leaveGroup(groupId: AccountId, deleteOnLeave: Boolean)
suspend fun promoteMember(group: AccountId, members: List<AccountId>) suspend fun promoteMember(group: AccountId, members: List<AccountId>)

View File

@ -120,7 +120,6 @@ class JobQueue : JobDelegate {
is NotifyPNServerJob, is NotifyPNServerJob,
is AttachmentUploadJob, is AttachmentUploadJob,
is GroupLeavingJob, is GroupLeavingJob,
is LibSessionGroupLeavingJob,
is MessageSendJob -> { is MessageSendJob -> {
txQueue.send(job) txQueue.send(job)
} }
@ -226,7 +225,6 @@ class JobQueue : JobDelegate {
RetrieveProfileAvatarJob.KEY, RetrieveProfileAvatarJob.KEY,
GroupLeavingJob.KEY, GroupLeavingJob.KEY,
InviteContactsJob.KEY, InviteContactsJob.KEY,
LibSessionGroupLeavingJob.KEY
) )
allJobTypes.forEach { type -> allJobTypes.forEach { type ->
resumePendingJobs(type) resumePendingJobs(type)

View File

@ -1,67 +0,0 @@
package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsignal.utilities.AccountId
class LibSessionGroupLeavingJob(val accountId: AccountId, val deleteOnLeave: Boolean): Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 4
override suspend fun execute(dispatcherName: String) {
val storage = MessagingModuleConfiguration.shared.storage
// start leaving
// create message ID with leaving state
val messageId = storage.insertGroupInfoLeaving(accountId) ?: run {
delegate?.handleJobFailedPermanently(
this,
dispatcherName,
Exception("Couldn't insert GroupInfoLeaving message in leaving group job")
)
return
}
// do actual group leave request
// on success
val leaveGroup = kotlin.runCatching {
MessagingModuleConfiguration.shared.groupManagerV2.leaveGroup(accountId, deleteOnLeave)
}
if (leaveGroup.isSuccess) {
// message is already deleted, succeed
delegate?.handleJobSucceeded(this, dispatcherName)
} else {
// Error leaving group, update the info message
storage.updateGroupInfoChange(messageId, UpdateMessageData.Kind.GroupErrorQuit)
}
}
override fun serialize(): Data =
Data.Builder()
.putString(SESSION_ID_KEY, accountId.hexString)
.putBoolean(DELETE_ON_LEAVE_KEY, deleteOnLeave)
.build()
class Factory : Job.Factory<LibSessionGroupLeavingJob> {
override fun create(data: Data): LibSessionGroupLeavingJob {
return LibSessionGroupLeavingJob(
AccountId(data.getString(SESSION_ID_KEY)),
data.getBoolean(DELETE_ON_LEAVE_KEY)
)
}
}
override fun getFactoryKey(): String = KEY
companion object {
const val KEY = "LibSessionGroupLeavingJob"
private const val SESSION_ID_KEY = "SessionId"
private const val DELETE_ON_LEAVE_KEY = "DeleteOnLeave"
}
}