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(
val id: AccountId,
val name: String?,
val destroyed: Boolean,
val deleteBefore: Long?,
val deleteAttachmentsBefore: Long?
) {
constructor(groupInfoConfig: ReadableGroupInfoConfig) : this(
id = groupInfoConfig.id(),
name = groupInfoConfig.getName(),
destroyed = groupInfoConfig.isDestroyed(),
deleteBefore = groupInfoConfig.getDeleteBefore(),
deleteAttachmentsBefore = groupInfoConfig.getDeleteAttachmentsBefore()
)
@ -212,11 +214,16 @@ class ConfigToDatabaseSync @Inject constructor(
val recipient = storage.getRecipientForThread(threadId) ?: return
recipientDatabase.setProfileName(recipient, groupInfoConfig.name)
profileManager.setName(context, recipient, groupInfoConfig.name ?: "")
groupInfoConfig.deleteBefore?.let { removeBefore ->
storage.trimThreadBefore(threadId, removeBefore)
}
groupInfoConfig.deleteAttachmentsBefore?.let { removeAttachmentsBefore ->
mmsDatabase.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true)
if (groupInfoConfig.destroyed) {
storage.clearMessages(threadId)
} else {
groupInfoConfig.deleteBefore?.let { removeBefore ->
storage.trimThreadBefore(threadId, removeBefore)
}
groupInfoConfig.deleteAttachmentsBefore?.let { removeAttachmentsBefore ->
mmsDatabase.deleteMessagesInThreadBeforeDate(threadId, removeAttachmentsBefore, onlyMedia = true)
}
}
}

View File

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

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
import android.app.Application
import android.content.Context
import android.view.MenuItem
import androidx.annotation.StringRes
import android.widget.Toast
import androidx.lifecycle.ViewModel
@ -13,11 +14,8 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -25,6 +23,7 @@ import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.database.MessageDataProvider
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.open_groups.OpenGroup
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.AccountId
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.ThreadDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.repository.ConversationRepository
@ -61,7 +62,9 @@ class ConversationViewModel(
private val groupDb: GroupDatabase,
private val threadDb: ThreadDatabase,
private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences
private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val groupManagerV2: GroupManagerV2,
) : ViewModel() {
val showSendAfterApprovalText: Boolean
@ -225,7 +228,7 @@ class ConversationViewModel(
*/
private fun shouldShowInput(recipient: Recipient?): Boolean {
return when {
recipient?.isClosedGroupV2Recipient == true -> !repository.isKicked(recipient)
recipient?.isClosedGroupV2Recipient == true -> !repository.isGroupReadOnly(recipient)
recipient?.isLegacyClosedGroupRecipient == 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
interface AssistedFactory {
fun create(threadId: Long, edKeyPair: KeyPair?): Factory
@ -890,7 +924,9 @@ class ConversationViewModel(
@ApplicationContext
private val context: Context,
private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences
private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val groupManagerV2: GroupManagerV2,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -904,7 +940,9 @@ class ConversationViewModel(
groupDb = groupDb,
threadDb = threadDb,
lokiMessageDb = lokiMessageDb,
textSecurePreferences = textSecurePreferences
textSecurePreferences = textSecurePreferences,
configFactory = configFactory,
groupManagerV2 = groupManagerV2,
) as T
}
}

View File

@ -20,6 +20,8 @@ import androidx.core.graphics.drawable.IconCompat
import com.squareup.phrase.Phrase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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(
context: Context,
item: MenuItem,
@ -164,7 +172,7 @@ object ConversationMenuHelper {
factory: ConfigFactory,
storage: StorageProtocol,
groupManager: GroupManagerV2,
): Boolean {
): ReceiveChannel<Unit>? {
when (item.itemId) {
R.id.menu_view_all_media -> { showAllMedia(context, thread) }
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_open_group_url -> { copyOpenGroupUrl(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_unmute_notifications -> { unmute(context, thread) }
R.id.menu_mute_notifications -> { mute(context, thread) }
R.id.menu_notification_settings -> { setNotifyType(context, thread) }
R.id.menu_call -> { call(context, thread) }
}
return true
return null
}
private fun showAllMedia(context: Context, thread: Recipient) {
@ -330,7 +339,7 @@ object ConversationMenuHelper {
configFactory: ConfigFactory,
storage: StorageProtocol,
groupManager: GroupManagerV2,
) {
): ReceiveChannel<Unit>? {
when {
thread.isLegacyClosedGroupRecipient -> {
val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
@ -357,11 +366,13 @@ object ConversationMenuHelper {
thread.isClosedGroupV2Recipient -> {
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) {
it.groupInfo.getName()
} ?: group.name
val channel = Channel<Unit>()
confirmAndLeaveClosedGroup(
context = context,
groupName = name,
@ -369,11 +380,19 @@ object ConversationMenuHelper {
threadID = threadID,
storage = storage,
doLeave = {
groupManager.leaveGroup(accountId, true)
try {
groupManager.leaveGroup(accountId, true)
} finally {
channel.send(Unit)
}
}
)
return channel
}
}
return null
}
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,22 +388,20 @@ class GroupManagerV2Impl @Inject constructor(
}
}
override suspend fun leaveGroup(group: AccountId, deleteOnLeave: Boolean) {
val canSendGroupMessage = configFactory.getClosedGroup(group)?.kicked == false
override suspend fun leaveGroup(groupId: AccountId, deleteOnLeave: Boolean) {
val group = configFactory.getClosedGroup(groupId)
if (canSendGroupMessage) {
val destination = Destination.ClosedGroup(group.hexString)
// 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 }.sessionId == storage.getUserPublicKey()
}
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.getDefaultInstance())
.build()
),
destination,
isSyncMessage = false
).await()
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()
@ -412,14 +410,40 @@ class GroupManagerV2Impl @Inject constructor(
),
destination,
isSyncMessage = false
).await()
)
// If we are not the only admin, send a left message for other admin to handle the member removal
if (!weAreTheOnlyAdmin) {
MessageSender.send(
GroupUpdated(
GroupUpdateMessage.newBuilder()
.setMemberLeftMessage(DataMessage.GroupUpdateMemberLeftMessage.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) {
storage.getThreadId(Address.fromSerialized(group.hexString))
storage.getThreadId(Address.fromSerialized(groupId.hexString))
?.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.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getClosedGroup
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.database.DatabaseContentProviders
@ -59,7 +60,7 @@ interface ConversationRepository {
fun deleteMessages(messages: Set<MessageRecord>, threadId: Long)
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
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 delete1on1MessagesRemotely(
@ -170,15 +171,16 @@ class DefaultConversationRepository @Inject constructor(
}
}
override fun isKicked(recipient: Recipient): Boolean {
// For now, we only know care we are kicked for a groups v2 recipient
override fun isGroupReadOnly(recipient: Recipient): Boolean {
// We only care about group v2 recipient
if (!recipient.isClosedGroupV2Recipient) {
return false
}
val groupId = recipient.address.serialize()
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

View File

@ -44,7 +44,7 @@ interface GroupManagerV2 {
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>)

View File

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