From 841bc69c3c150c30aa03839f2eff98a77b4b9556 Mon Sep 17 00:00:00 2001
From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com>
Date: Thu, 12 Sep 2024 16:40:19 +1000
Subject: [PATCH] Remove conversation settings and added back group operation
---
 app/src/main/AndroidManifest.xml              |   7 -
 ...onversationNotificationSettingsActivity.kt |  59 ---
 ...ionNotificationSettingsActivityContract.kt |  16 -
 .../settings/ConversationSettingsActivity.kt  | 269 -------------
 .../ConversationSettingsActivityContract.kt   |  24 --
 .../settings/ConversationSettingsViewModel.kt | 104 -----
 .../conversation/v2/ConversationActivityV2.kt |  32 +-
 .../v2/menus/ConversationMenuHelper.kt        | 100 ++++-
 ...ity_conversation_notification_settings.xml |  73 ----
 .../layout/activity_conversation_settings.xml | 375 ------------------
 .../res/menu/menu_conversation_groups_v2.xml  |  12 +
 .../menu_conversation_groups_v2_admin.xml     |  12 +
 ...xml => menu_conversation_legacy_group.xml} |   0
 13 files changed, 111 insertions(+), 972 deletions(-)
 delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationNotificationSettingsActivity.kt
 delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationNotificationSettingsActivityContract.kt
 delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivity.kt
 delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivityContract.kt
 delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsViewModel.kt
 delete mode 100644 app/src/main/res/layout/activity_conversation_notification_settings.xml
 delete mode 100644 app/src/main/res/layout/activity_conversation_settings.xml
 create mode 100644 app/src/main/res/menu/menu_conversation_groups_v2.xml
 create mode 100644 app/src/main/res/menu/menu_conversation_groups_v2_admin.xml
 rename app/src/main/res/menu/{menu_conversation_closed_group.xml => menu_conversation_legacy_group.xml} (100%)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9687c01143..eeead1c3ce 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -243,13 +243,6 @@
                 android:name="android.support.PARENT_ACTIVITY"
                 android:value="org.thoughtcrime.securesms.home.HomeActivity" />
         
-        
-        
         () {
-
-    override fun createIntent(context: Context, input: Long): Intent =
-        Intent(context, ConversationNotificationSettingsActivity::class.java).apply {
-            putExtra(ConversationActivityV2.THREAD_ID, input)
-        }
-
-    override fun parseResult(resultCode: Int, intent: Intent?) { /* do nothing */ }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivity.kt
deleted file mode 100644
index a44b3fc55d..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivity.kt
+++ /dev/null
@@ -1,269 +0,0 @@
-package org.thoughtcrime.securesms.conversation.settings
-
-import android.content.Intent
-import android.os.Bundle
-import android.view.View
-import androidx.activity.viewModels
-import androidx.core.view.isVisible
-import androidx.lifecycle.lifecycleScope
-import com.squareup.phrase.Phrase
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.launch
-import network.loki.messenger.R
-import network.loki.messenger.databinding.ActivityConversationSettingsBinding
-import org.session.libsession.messaging.sending_receiving.MessageSender
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.GroupUtil
-import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
-import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.Log
-import org.session.libsignal.utilities.toHexString
-import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
-import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
-import org.thoughtcrime.securesms.database.GroupDatabase
-import org.thoughtcrime.securesms.database.LokiThreadDatabase
-import org.thoughtcrime.securesms.database.ThreadDatabase
-import org.thoughtcrime.securesms.groups.EditGroupActivity
-import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity
-import org.thoughtcrime.securesms.media.MediaOverviewActivity
-import org.thoughtcrime.securesms.showSessionDialog
-import java.io.IOException
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.OnClickListener {
-
-    companion object {
-        // used to trigger displaying conversation search in calling parent activity
-        const val RESULT_SEARCH = 22
-    }
-
-    lateinit var binding: ActivityConversationSettingsBinding
-
-    private val groupOptions: List
-    get() = with(binding) {
-        listOf(
-            groupMembers,
-            groupMembersDivider.root,
-            editGroup,
-            editGroupDivider.root,
-            leaveGroup,
-            leaveGroupDivider.root
-        )
-    }
-
-    @Inject lateinit var threadDb: ThreadDatabase
-    @Inject lateinit var groupDb: GroupDatabase
-    @Inject lateinit var lokiThreadDb: LokiThreadDatabase
-    @Inject lateinit var viewModelFactory: ConversationSettingsViewModel.AssistedFactory
-    val viewModel: ConversationSettingsViewModel by viewModels {
-        val threadId = intent.getLongExtra(ConversationActivityV2.THREAD_ID, -1L)
-        if (threadId == -1L) {
-            finish()
-        }
-        viewModelFactory.create(threadId)
-    }
-
-    private val notificationActivityCallback = registerForActivityResult(ConversationNotificationSettingsActivityContract()) {
-        updateRecipientDisplay()
-    }
-
-    override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
-        super.onCreate(savedInstanceState, ready)
-        binding = ActivityConversationSettingsBinding.inflate(layoutInflater)
-        setContentView(binding.root)
-        updateRecipientDisplay()
-        binding.searchConversation.setOnClickListener(this)
-        binding.clearMessages.setOnClickListener(this)
-        binding.allMedia.setOnClickListener(this)
-        binding.pinConversation.setOnClickListener(this)
-        binding.notificationSettings.setOnClickListener(this)
-        binding.editGroup.setOnClickListener(this)
-        binding.leaveGroup.setOnClickListener(this)
-        binding.back.setOnClickListener(this)
-        binding.autoDownloadMediaSwitch.setOnCheckedChangeListener { _, isChecked ->
-            viewModel.setAutoDownloadAttachments(isChecked)
-            updateRecipientDisplay()
-        }
-    }
-
-    private fun updateRecipientDisplay() {
-        val recipient = viewModel.recipient ?: return
-        // Setup profile image
-        binding.profilePictureView.root.update(recipient)
-        // Setup name
-        binding.conversationName.text = when {
-            recipient.isLocalNumber -> getString(R.string.noteToSelf)
-            else -> recipient.toShortString()
-        }
-        // Setup group description (if group)
-        binding.conversationSubtitle.isVisible = recipient.isClosedGroupV2Recipient.apply {
-            binding.conversationSubtitle.text = viewModel.closedGroupInfo()?.description
-        }
-
-        // Toggle group-specific settings
-        val areGroupOptionsVisible = recipient.isClosedGroupV2Recipient || recipient.isLegacyClosedGroupRecipient
-        groupOptions.forEach { v ->
-            v.isVisible = areGroupOptionsVisible
-        }
-
-        // Group admin settings
-        val isUserGroupAdmin = areGroupOptionsVisible && viewModel.isUserGroupAdmin()
-        with (binding) {
-            groupMembersDivider.root.isVisible = areGroupOptionsVisible && !isUserGroupAdmin
-            groupMembers.isVisible = !isUserGroupAdmin
-            adminControlsGroup.isVisible = isUserGroupAdmin
-            deleteGroup.isVisible = isUserGroupAdmin
-            clearMessages.isVisible = isUserGroupAdmin
-            clearMessagesDivider.root.isVisible = isUserGroupAdmin
-            leaveGroupDivider.root.isVisible = isUserGroupAdmin
-        }
-
-        // Set pinned state
-        binding.pinConversation.setText(
-            if (viewModel.isPinned()) R.string.pinUnpinConversation
-            else R.string.pinConversation
-        )
-
-        // Set auto-download state
-        val trusted = viewModel.autoDownloadAttachments()
-        binding.autoDownloadMediaSwitch.isChecked = trusted
-
-        // Set notification type
-        val notifyTypes = resources.getStringArray(R.array.notify_types)
-        val summary = notifyTypes.getOrNull(recipient.notifyType)
-        binding.notificationsValue.text = summary
-    }
-
-    override fun onClick(v: View?) {
-        val threadRecipient = viewModel.recipient ?: return
-        when {
-            v === binding.searchConversation -> {
-                setResult(RESULT_SEARCH)
-                finish()
-            }
-            v === binding.allMedia -> {
-                startActivity(MediaOverviewActivity.createIntent(this, threadRecipient.address))
-            }
-            v === binding.pinConversation -> {
-                viewModel.togglePin().invokeOnCompletion { e ->
-                    if (e != null) {
-                        // something happened
-                        Log.e("ConversationSettings", "Failed to toggle pin on thread", e)
-                    } else {
-                        updateRecipientDisplay()
-                    }
-                }
-            }
-            v === binding.notificationSettings -> {
-                notificationActivityCallback.launch(viewModel.threadId)
-            }
-            v === binding.back -> onBackPressed()
-            v === binding.clearMessages -> {
-
-                showSessionDialog {
-                    title(R.string.clearMessages)
-                    text(Phrase.from(this@ConversationSettingsActivity, R.string.clearMessagesChatDescription)
-                        .put(NAME_KEY, threadRecipient.name)
-                        .format())
-                    dangerButton(
-                        R.string.clear,
-                        R.string.clear) {
-                        viewModel.clearMessages(false)
-                    }
-                    cancelButton()
-                }
-            }
-            v === binding.leaveGroup -> {
-
-                if (threadRecipient.isLegacyClosedGroupRecipient) {
-                    // Send a leave group message if this is an active closed group
-                    val groupString = threadRecipient.address.toGroupString()
-                    val ourId = TextSecurePreferences.getLocalNumber(this)!!
-                    if (groupDb.isActive(groupString)) {
-                        showSessionDialog {
-
-                            title(R.string.groupLeave)
-
-                            val name = viewModel.recipient!!.name!!
-                            val textWithArgs = if (groupDb.getGroup(groupString).get().admins.map(Address::serialize).contains(ourId)) {
-                                Phrase.from(context, R.string.groupLeaveDescriptionAdmin)
-                                    .put(GROUP_NAME_KEY, name)
-                                    .format()
-                            } else {
-                                Phrase.from(context, R.string.groupLeaveDescription)
-                                    .put(GROUP_NAME_KEY, name)
-                                    .format()
-                            }
-                            text(textWithArgs)
-                            dangerButton(
-                                R.string.groupLeave,
-                                R.string.groupLeave
-                            ) {
-                                lifecycleScope.launch {
-                                    GroupUtil.doubleDecodeGroupID(threadRecipient.address.toString())
-                                        .toHexString()
-                                        .let { MessageSender.explicitLeave(it, true, deleteThread = true) }
-                                    finish()
-                                }
-                            }
-                            cancelButton()
-                        }
-                        try {
-
-                        } catch (e: IOException) {
-                            Log.e("Loki", e)
-                        }
-                    }
-                } else if (threadRecipient.isClosedGroupV2Recipient) {
-                    val groupInfo = viewModel.closedGroupInfo()
-                    showSessionDialog {
-
-                        title(R.string.groupLeave)
-
-                        val name = viewModel.recipient!!.name!!
-                        val textWithArgs = if (groupInfo?.isUserAdmin == true) {
-                            Phrase.from(context, R.string.groupLeaveDescription)
-                                .put(GROUP_NAME_KEY, name)
-                                .format()
-                        } else {
-                            Phrase.from(context, R.string.groupLeaveDescription)
-                                .put(GROUP_NAME_KEY, name)
-                                .format()
-                        }
-                        text(textWithArgs)
-                        dangerButton(
-                            R.string.groupLeave,
-                            R.string.groupLeave
-                        ) {
-                            lifecycleScope.launch {
-                                viewModel.leaveGroup()
-                                finish()
-                            }
-                        }
-                        cancelButton()
-                    }
-                }
-            }
-            v === binding.editGroup -> {
-                val recipient = viewModel.recipient ?: return
-
-                val intent = when {
-                    recipient.isLegacyClosedGroupRecipient -> Intent(this, EditLegacyGroupActivity::class.java).apply {
-                        val groupID: String = recipient.address.toGroupString()
-                        putExtra(EditLegacyGroupActivity.groupIDKey, groupID)
-                    }
-
-                    recipient.isClosedGroupV2Recipient -> EditGroupActivity.createIntent(
-                        context = this,
-                        groupSessionId = recipient.address.serialize()
-                    )
-
-                    else -> return
-                }
-                startActivity(intent)
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivityContract.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivityContract.kt
deleted file mode 100644
index a79d94b3ae..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsActivityContract.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.thoughtcrime.securesms.conversation.settings
-
-import android.content.Context
-import android.content.Intent
-import androidx.activity.result.contract.ActivityResultContract
-import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
-
-sealed class ConversationSettingsActivityResult {
-    object Finished: ConversationSettingsActivityResult()
-    object SearchConversation: ConversationSettingsActivityResult()
-}
-
-class ConversationSettingsActivityContract: ActivityResultContract() {
-
-    override fun createIntent(context: Context, input: Long) = Intent(context, ConversationSettingsActivity::class.java).apply {
-        putExtra(ConversationActivityV2.THREAD_ID, input ?: -1L)
-    }
-
-    override fun parseResult(resultCode: Int, intent: Intent?): ConversationSettingsActivityResult =
-        when (resultCode) {
-            ConversationSettingsActivity.RESULT_SEARCH -> ConversationSettingsActivityResult.SearchConversation
-            else -> ConversationSettingsActivityResult.Finished
-        }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsViewModel.kt
deleted file mode 100644
index 17411b2211..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/settings/ConversationSettingsViewModel.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.thoughtcrime.securesms.conversation.settings
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.viewModelScope
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import network.loki.messenger.libsession_util.util.GroupDisplayInfo
-import org.session.libsession.database.StorageProtocol
-import org.session.libsession.messaging.jobs.JobQueue
-import org.session.libsession.messaging.jobs.LibSessionGroupLeavingJob
-import org.session.libsession.utilities.Address
-import org.session.libsession.utilities.TextSecurePreferences
-import org.session.libsignal.utilities.AccountId
-
-class ConversationSettingsViewModel(
-    val threadId: Long,
-    private val storage: StorageProtocol,
-    private val prefs: TextSecurePreferences
-): ViewModel() {
-
-    val recipient get() = storage.getRecipientForThread(threadId)
-
-    fun isPinned() = storage.isPinned(threadId)
-
-    fun togglePin() = viewModelScope.launch {
-        val isPinned = storage.isPinned(threadId)
-        storage.setPinned(threadId, !isPinned)
-    }
-
-    fun autoDownloadAttachments() = recipient?.let { recipient -> storage.shouldAutoDownloadAttachments(recipient) } ?: false
-
-    fun setAutoDownloadAttachments(shouldDownload: Boolean) {
-        recipient?.let { recipient -> storage.setAutoDownloadAttachments(recipient, shouldDownload) }
-    }
-
-    fun isUserGroupAdmin(): Boolean = recipient?.let { recipient ->
-        when {
-            recipient.isLegacyClosedGroupRecipient -> {
-                val localUserAddress = prefs.getLocalNumber() ?: return@let false
-                val group = storage.getGroup(recipient.address.toGroupString())
-                group?.admins?.contains(Address.fromSerialized(localUserAddress)) ?: false // this will have to be replaced for new closed groups
-            }
-            recipient.isClosedGroupV2Recipient -> {
-                val group = storage.getLibSessionClosedGroup(recipient.address.serialize()) ?: return@let false
-                group.adminKey != null
-            }
-            else -> false
-        }
-    } ?: false
-
-    fun clearMessages(forAll: Boolean) {
-        if (forAll && !isUserGroupAdmin()) return
-
-        if (!forAll) {
-            viewModelScope.launch {
-                storage.clearMessages(threadId)
-            }
-        } else {
-            // do a send message here and on success do a clear messages
-            viewModelScope.launch {
-                storage.clearMessages(threadId)
-            }
-        }
-    }
-
-    fun closedGroupInfo(): GroupDisplayInfo? = recipient
-        ?.address
-        ?.takeIf { it.isClosedGroupV2 }
-        ?.serialize()
-        ?.let(storage::getClosedGroupDisplayInfo)
-
-    // Assume that user has verified they don't want to add a new admin etc
-    suspend fun leaveGroup() {
-        val recipient = recipient ?: return
-        return withContext(Dispatchers.IO) {
-            val groupLeave = LibSessionGroupLeavingJob(
-                AccountId(recipient.address.serialize()),
-                true
-            )
-            JobQueue.shared.add(groupLeave)
-        }
-    }
-
-    // DI-related
-    @dagger.assisted.AssistedFactory
-    interface AssistedFactory {
-        fun create(threadId: Long): Factory
-    }
-    class Factory @AssistedInject constructor(
-        @Assisted private val threadId: Long,
-        private val storage: StorageProtocol,
-        private val prefs: TextSecurePreferences
-    ) : ViewModelProvider.Factory {
-        @Suppress("UNCHECKED_CAST")
-        override fun  create(modelClass: Class): T {
-            return ConversationSettingsViewModel(threadId, storage, prefs) as T
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index 345253bda1..7f526c518d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -119,8 +119,6 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
 import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
 import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
 import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
-import org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActivityContract
-import org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActivityResult
 import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
 import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
 import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
@@ -164,6 +162,7 @@ import org.thoughtcrime.securesms.database.model.MessageId
 import org.thoughtcrime.securesms.database.model.MessageRecord
 import org.thoughtcrime.securesms.database.model.MmsMessageRecord
 import org.thoughtcrime.securesms.database.model.ReactionRecord
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
 import org.thoughtcrime.securesms.giph.ui.GiphyActivity
 import org.thoughtcrime.securesms.groups.OpenGroupManager
 import org.thoughtcrime.securesms.home.search.getSearchName
@@ -222,7 +221,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
     ConversationActionModeCallbackDelegate, VisibleMessageViewDelegate, RecipientModifiedListener,
     SearchBottomBar.EventListener, LoaderManager.LoaderCallbacks, ConversationActionBarDelegate,
     OnReactionSelectedListener, ReactWithAnyEmojiDialogFragment.Callback, ReactionsDialogFragment.Callback,
-    ConversationMenuHelper.ConversationMenuListener, View.OnClickListener {
+    ConversationMenuHelper.ConversationMenuListener {
 
     private lateinit var binding: ActivityConversationV2Binding
 
@@ -239,6 +238,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
     @Inject lateinit var reactionDb: ReactionDatabase
     @Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory
     @Inject lateinit var mentionViewModelFactory: MentionViewModel.AssistedFactory
+    @Inject lateinit var configFactory: ConfigFactory
 
     private val screenshotObserver by lazy {
         ScreenshotObserver(this, Handler(Looper.getMainLooper())) {
@@ -247,13 +247,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
         }
     }
 
-    private val conversationSettingsCallback = registerForActivityResult(ConversationSettingsActivityContract()) { result ->
-        if (result is ConversationSettingsActivityResult.SearchConversation) {
-            // open search
-            binding?.toolbar?.menu?.findItem(R.id.menu_search)?.expandActionView()
-        }
-    }
-
     private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
     private val linkPreviewViewModel: LinkPreviewViewModel by lazy {
         ViewModelProvider(this, LinkPreviewViewModel.Factory(LinkPreviewRepository()))
@@ -497,7 +490,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
         updatePlaceholder()
         setUpBlockedBanner()
         binding.searchBottomBar.setEventListener(this)
-        binding.toolbarContent.profilePictureView.setOnClickListener(this)
         updateSendAfterApprovalText()
         setUpMessageRequests()
 
@@ -949,10 +941,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
         val recipient = viewModel.recipient ?: return false
         if (!viewModel.isMessageRequestThread) {
             ConversationMenuHelper.onPrepareOptionsMenu(
-                menu,
-                menuInflater,
-                recipient,
-                this
+                menu = menu,
+                inflater = menuInflater,
+                thread = recipient,
+                context = this,
+                configFactory = configFactory,
             )
         }
         maybeUpdateToolbar(recipient)
@@ -1219,17 +1212,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
             return false
         }
         return viewModel.recipient?.let { recipient ->
-            ConversationMenuHelper.onOptionItemSelected(this, item, recipient)
+            ConversationMenuHelper.onOptionItemSelected(this, item, recipient, configFactory, storage)
         } ?: false
     }
 
-    override fun onClick(v: View?) {
-        if (v === binding?.toolbarContent?.profilePictureView) {
-            // open conversation settings
-            conversationSettingsCallback.launch(viewModel.threadId)
-        }
-    }
-
     override fun block(deleteThread: Boolean) {
         val recipient = viewModel.recipient ?: return Log.w("Loki", "Recipient was null for block action")
         val invitingAdmin = viewModel.invitingAdmin
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
index d16c46946b..4a0cf31d00 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
 import android.content.Context
 import android.content.Intent
 import android.graphics.BitmapFactory
-import android.net.Uri
 import android.os.AsyncTask
 import android.view.Menu
 import android.view.MenuInflater
@@ -21,13 +20,14 @@ import androidx.core.graphics.drawable.IconCompat
 import com.squareup.phrase.Phrase
 import java.io.IOException
 import network.loki.messenger.R
+import org.session.libsession.database.StorageProtocol
 import org.session.libsession.messaging.sending_receiving.MessageSender
 import org.session.libsession.messaging.sending_receiving.leave
 import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
-import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
 import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
 import org.session.libsession.utilities.TextSecurePreferences
 import org.session.libsession.utilities.recipients.Recipient
+import org.session.libsignal.utilities.AccountId
 import org.session.libsignal.utilities.Log
 import org.session.libsignal.utilities.guava.Optional
 import org.session.libsignal.utilities.toHexString
@@ -38,6 +38,8 @@ import org.thoughtcrime.securesms.calls.WebRtcCallActivity
 import org.thoughtcrime.securesms.contacts.SelectContactsActivity
 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
 import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
+import org.thoughtcrime.securesms.database.Storage
+import org.thoughtcrime.securesms.dependencies.ConfigFactory
 import org.thoughtcrime.securesms.dependencies.DatabaseComponent
 import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity
 import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity.Companion.groupIDKey
@@ -54,7 +56,8 @@ object ConversationMenuHelper {
         menu: Menu,
         inflater: MenuInflater,
         thread: Recipient,
-        context: Context
+        context: Context,
+        configFactory: ConfigFactory,
     ) {
         // Prepare
         menu.clear()
@@ -77,10 +80,20 @@ object ConversationMenuHelper {
                 inflater.inflate(R.menu.menu_conversation_block, menu)
             }
         }
-        // Closed group menu (options that should only be present in closed groups)
+        // (Legacy) Closed group menu (options that should only be present in closed groups)
         if (thread.isLegacyClosedGroupRecipient) {
-            inflater.inflate(R.menu.menu_conversation_closed_group, menu)
+            inflater.inflate(R.menu.menu_conversation_legacy_group, menu)
         }
+
+        // Groups v2 menu
+        if (thread.isClosedGroupV2Recipient) {
+            if (configFactory.userGroups?.getClosedGroup(thread.address.serialize())?.hasAdminKey() == true) {
+                inflater.inflate(R.menu.menu_conversation_groups_v2_admin, menu)
+            }
+
+            inflater.inflate(R.menu.menu_conversation_groups_v2, menu)
+        }
+
         // Open group menu
         if (isCommunity) {
             inflater.inflate(R.menu.menu_conversation_open_group, menu)
@@ -134,7 +147,13 @@ object ConversationMenuHelper {
         })
     }
 
-    fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean {
+    fun onOptionItemSelected(
+        context: Context,
+        item: MenuItem,
+        thread: Recipient,
+        factory: ConfigFactory,
+        storage: StorageProtocol
+    ): Boolean {
         when (item.itemId) {
             R.id.menu_view_all_media -> { showAllMedia(context, thread) }
             R.id.menu_search -> { search(context) }
@@ -146,7 +165,7 @@ 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) }
+            R.id.menu_leave_group -> { leaveClosedGroup(context, thread, factory, storage) }
             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) }
@@ -278,26 +297,67 @@ object ConversationMenuHelper {
         context.startActivity(intent)
     }
 
-    private fun leaveClosedGroup(context: Context, thread: Recipient) {
-        if (!thread.isLegacyClosedGroupRecipient) { return }
+    private fun leaveClosedGroup(
+        context: Context,
+        thread: Recipient,
+        configFactory: ConfigFactory,
+        storage: StorageProtocol
+    ) {
+        when {
+            thread.isLegacyClosedGroupRecipient -> {
+                val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
+                val admins = group.admins
+                val accountID = TextSecurePreferences.getLocalNumber(context)
+                val isCurrentUserAdmin = admins.any { it.toString() == accountID }
 
-        val group = DatabaseComponent.get(context).groupDatabase().getGroup(thread.address.toGroupString()).orNull()
-        val admins = group.admins
-        val accountID = TextSecurePreferences.getLocalNumber(context)
-        val isCurrentUserAdmin = admins.any { it.toString() == accountID }
-        val message = if (isCurrentUserAdmin) {
+                confirmAndLeaveClosedGroup(context, group.title, isCurrentUserAdmin, doLeave = {
+                    val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
+
+                    check(DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)) {
+                        "Invalid group public key"
+                    }
+                    MessageSender.leave(groupPublicKey, notifyUser = false)
+                })
+            }
+            
+            thread.isClosedGroupV2Recipient -> {
+                val accountId = AccountId(thread.address.serialize())
+                val group = configFactory.userGroups?.getClosedGroup(accountId.hexString) ?: return
+                val (name, isAdmin) = configFactory.getGroupInfoConfig(accountId)?.use {
+                    it.getName() to group.hasAdminKey()
+                } ?: return
+
+                confirmAndLeaveClosedGroup(
+                    context = context,
+                    groupName = name,
+                    isAdmin = isAdmin,
+                    doLeave = {
+                        check(storage.leaveGroup(accountId.hexString, true))
+                    }
+                )
+            }
+        }
+    }
+
+    private fun confirmAndLeaveClosedGroup(
+        context: Context,
+        groupName: String,
+        isAdmin: Boolean,
+        doLeave: () -> Unit,
+    ) {
+        val message = if (isAdmin) {
             Phrase.from(context, R.string.groupDeleteDescription)
-                .put(GROUP_NAME_KEY, group.title)
+                .put(GROUP_NAME_KEY, groupName)
                 .format()
         } else {
             Phrase.from(context, R.string.groupLeaveDescription)
-                .put(GROUP_NAME_KEY, group.title)
+                .put(GROUP_NAME_KEY, groupName)
                 .format()
         }
 
         fun onLeaveFailed() {
             val txt = Phrase.from(context, R.string.groupLeaveErrorFailed)
-                .put(GROUP_NAME_KEY, group.title)
+                .put(GROUP_NAME_KEY, groupName)
                 .format().toString()
             Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
         }
@@ -307,11 +367,7 @@ object ConversationMenuHelper {
             text(message)
             dangerButton(R.string.leave) {
                 try {
-                    val groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString()
-                    val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(groupPublicKey)
-
-                    if (isClosedGroup) MessageSender.leave(groupPublicKey, notifyUser = false)
-                    else onLeaveFailed()
+                    doLeave()
                 } catch (e: Exception) {
                     onLeaveFailed()
                 }
diff --git a/app/src/main/res/layout/activity_conversation_notification_settings.xml b/app/src/main/res/layout/activity_conversation_notification_settings.xml
deleted file mode 100644
index 74ba7632e1..0000000000
--- a/app/src/main/res/layout/activity_conversation_notification_settings.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-
-
-
-    
-    
-    
-    
-    
-    
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_conversation_settings.xml b/app/src/main/res/layout/activity_conversation_settings.xml
deleted file mode 100644
index d5f810d2cb..0000000000
--- a/app/src/main/res/layout/activity_conversation_settings.xml
+++ /dev/null
@@ -1,375 +0,0 @@
-
-
-    
-
-        
-
-        
-
-        
-
-        
-
-        
-        
-            
-            
-            
-            
-            
-            
-            
-            
-            
-                
-                
-                    
-                    
-                
-            
-            
-            
-                
-
-                    
-                
-                
-                    
-                    
-                
-            
-        
-
-        
-        
-        
-
-        
-            
-            
-            
-            
-                
-                
-                    
-                    
-                
-            
-        
-
-        
-            
-            
-            
-            
-            
-        
-
-
-
-    
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_conversation_groups_v2.xml b/app/src/main/res/menu/menu_conversation_groups_v2.xml
new file mode 100644
index 0000000000..e519278931
--- /dev/null
+++ b/app/src/main/res/menu/menu_conversation_groups_v2.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_conversation_groups_v2_admin.xml b/app/src/main/res/menu/menu_conversation_groups_v2_admin.xml
new file mode 100644
index 0000000000..2cab0f9c5b
--- /dev/null
+++ b/app/src/main/res/menu/menu_conversation_groups_v2_admin.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_conversation_closed_group.xml b/app/src/main/res/menu/menu_conversation_legacy_group.xml
similarity index 100%
rename from app/src/main/res/menu/menu_conversation_closed_group.xml
rename to app/src/main/res/menu/menu_conversation_legacy_group.xml