Remove conversation settings and added back group operation

This commit is contained in:
SessionHero01 2024-09-12 16:40:19 +10:00
parent cc64f461ef
commit 841bc69c3c
No known key found for this signature in database
13 changed files with 111 additions and 972 deletions

View File

@ -243,13 +243,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" />
</activity>
<activity android:name="org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight.NoActionBar"/>
<activity android:name="org.thoughtcrime.securesms.conversation.settings.ConversationNotificationSettingsActivity"
android:label="@string/sessionNotifications"
android:screenOrientation="portrait"
android:theme="@style/Theme.Session.DayNight"/>
<activity
android:name="org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity"
android:screenOrientation="portrait"

View File

@ -1,59 +0,0 @@
package org.thoughtcrime.securesms.conversation.settings
import android.os.Bundle
import android.view.View
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.databinding.ActivityConversationNotificationSettingsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import javax.inject.Inject
@AndroidEntryPoint
class ConversationNotificationSettingsActivity: PassphraseRequiredActionBarActivity(), View.OnClickListener {
lateinit var binding: ActivityConversationNotificationSettingsBinding
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var recipientDb: RecipientDatabase
val recipient by lazy {
if (threadId == -1L) null
else threadDb.getRecipientForThreadId(threadId)
}
var threadId: Long = -1
override fun onClick(v: View?) {
val recipient = recipient ?: return
if (v === binding.notifyAll) {
// set notify type
recipientDb.setNotifyType(recipient, RecipientDatabase.NOTIFY_TYPE_ALL)
} else if (v === binding.notifyMentions) {
recipientDb.setNotifyType(recipient, RecipientDatabase.NOTIFY_TYPE_MENTIONS)
} else if (v === binding.notifyMute) {
recipientDb.setNotifyType(recipient, RecipientDatabase.NOTIFY_TYPE_NONE)
}
updateValues()
}
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
binding = ActivityConversationNotificationSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
threadId = intent.getLongExtra(ConversationActivityV2.THREAD_ID, -1L)
if (threadId == -1L) finish()
updateValues()
with (binding) {
notifyAll.setOnClickListener(this@ConversationNotificationSettingsActivity)
notifyMentions.setOnClickListener(this@ConversationNotificationSettingsActivity)
notifyMute.setOnClickListener(this@ConversationNotificationSettingsActivity)
}
}
private fun updateValues() {
val notifyType = recipient?.notifyType ?: return
binding.notifyAllButton.isSelected = notifyType == RecipientDatabase.NOTIFY_TYPE_ALL
binding.notifyMentionsButton.isSelected = notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS
binding.notifyMuteButton.isSelected = notifyType == RecipientDatabase.NOTIFY_TYPE_NONE
}
}

View File

@ -1,16 +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
class ConversationNotificationSettingsActivityContract: ActivityResultContract<Long, Unit>() {
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 */ }
}

View File

@ -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<View>
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)
}
}
}
}

View File

@ -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<Long, ConversationSettingsActivityResult>() {
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
}
}

View File

@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
return ConversationSettingsViewModel(threadId, storage, prefs) as T
}
}
}

View File

@ -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<Cursor>, 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

View File

@ -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 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()
}

View File

@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:background="@drawable/preference_top"
android:paddingTop="@dimen/small_spacing"
android:id="@+id/notifyAll"
style="@style/TextAppearance.Session.ConversationSettings.Option"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/notificationsAllMessages"
android:layout_width="0dp"
android:layout_height="72dp"/>
<View
android:layout_marginTop="@dimen/small_spacing"
app:layout_constraintTop_toTopOf="@+id/notifyAll"
app:layout_constraintBottom_toBottomOf="@+id/notifyAll"
app:layout_constraintEnd_toEndOf="@+id/notifyAll"
android:layout_marginEnd="54dp"
android:id="@+id/notifyAllButton"
android:padding="@dimen/small_spacing"
android:layout_width="@dimen/small_radial_size"
android:layout_height="@dimen/small_radial_size"
android:background="@drawable/padded_circle_accent_select"
android:foreground="@drawable/radial_multi_select"/>
<TextView
app:layout_constraintTop_toBottomOf="@+id/notifyAll"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/notifyMentions"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:background="@drawable/preference_middle"
android:text="@string/notificationsMentionsOnly"
android:layout_width="0dp"
android:layout_height="@dimen/setting_button_height"/>
<View
app:layout_constraintTop_toTopOf="@+id/notifyMentions"
app:layout_constraintBottom_toBottomOf="@+id/notifyMentions"
app:layout_constraintEnd_toEndOf="@+id/notifyMentions"
android:layout_marginEnd="54dp"
android:id="@+id/notifyMentionsButton"
android:layout_width="@dimen/small_radial_size"
android:layout_height="@dimen/small_radial_size"
android:background="@drawable/padded_circle_accent_select"
android:foreground="@drawable/radial_multi_select"/>
<TextView
android:paddingBottom="@dimen/small_spacing"
app:layout_constraintTop_toBottomOf="@+id/notifyMentions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/notifyMute"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:background="@drawable/preference_bottom"
android:text="@string/notificationsMute"
android:layout_width="0dp"
android:layout_height="72dp"/>
<View
android:layout_marginBottom="@dimen/small_spacing"
app:layout_constraintTop_toTopOf="@+id/notifyMute"
app:layout_constraintBottom_toBottomOf="@+id/notifyMute"
app:layout_constraintEnd_toEndOf="@+id/notifyMute"
android:layout_marginEnd="54dp"
android:id="@+id/notifyMuteButton"
android:layout_width="@dimen/small_radial_size"
android:layout_height="@dimen/small_radial_size"
android:background="@drawable/padded_circle_accent_select"
android:foreground="@drawable/radial_multi_select"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,375 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.thoughtcrime.securesms.conversation.settings.ConversationSettingsActivity"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/back"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/ic_baseline_close_24"
android:scaleType="centerInside"
android:layout_width="?android:actionBarSize"
android:layout_height="?android:actionBarSize"
app:tint="?android:textColorPrimary" />
<include
android:id="@+id/profilePictureView"
layout="@layout/view_large_profile_picture"
android:layout_height="120dp"
android:layout_width="120dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="@dimen/small_profile_picture_size"
/>
<TextView
android:id="@+id/conversationName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/massive_spacing"
android:layout_marginTop="@dimen/small_spacing"
style="@style/TextAppearance.Session.ConversationSettings.Title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profilePictureView"
tools:text="@tools:sample/full_names" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/conversationSubtitle"
app:layout_constraintTop_toBottomOf="@id/conversationName"
android:layout_marginHorizontal="@dimen/massive_spacing"
style="@style/TextAppearance.Session.ConversationSettings.Subtitle"
tools:text="@tools:sample/lorem/random"
android:maxLines="2"
android:ellipsize="end"
/>
<!-- Main conversation settings -->
<LinearLayout
android:id="@+id/mainConversationSettingContainer"
android:background="@drawable/preference_single_no_padding"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/conversationSubtitle"
android:layout_marginTop="@dimen/large_spacing"
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@drawable/debug_border"
android:orientation="vertical">
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_search_conversation"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:text="@string/searchConversation"
android:id="@+id/searchConversation"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_edit_group"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:text="@string/groupMembers"
android:id="@+id/groupMembers"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/groupMembersDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_all_media"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:text="@string/conversationsSettingsAllMedia"
android:id="@+id/allMedia"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_pin_conversation"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:text="@string/pinConversation"
android:id="@+id/pinConversation"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<LinearLayout
android:id="@+id/notificationSettings"
android:background="?selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:paddingHorizontal="@dimen/very_large_spacing"
android:orientation="horizontal">
<ImageView
android:src="@drawable/ic_notification_settings"
android:layout_gravity="center"
android:layout_width="@dimen/setting_image_size"
android:layout_height="@dimen/setting_image_size"
app:tint="?android:textColorPrimary" />
<LinearLayout
android:layout_marginStart="@dimen/very_large_spacing"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sessionNotifications"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:paddingVertical="1dp"
style="@style/TextAppearance.Session.ConversationSettings.Option"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/notificationsValue"
tools:text="@tools:sample/lorem"
android:paddingVertical="1dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
style="@style/TextAppearance.Session.ConversationSettings.OptionSummary"/>
</LinearLayout>
</LinearLayout>
<include
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/switchContainer"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_width="96dp"
android:layout_height="match_parent">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/autoDownloadMediaSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:minHeight="48dp" />
</FrameLayout>
<LinearLayout
android:id="@+id/autoDownloadMediaContainer"
android:layout_toEndOf="@+id/switchContainer"
android:layout_alignParentEnd="true"
android:layout_marginEnd="@dimen/very_large_spacing"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_width="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/attachmentsAutoDownload"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:paddingVertical="1dp"
style="@style/TextAppearance.Session.ConversationSettings.Option"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/attachmentsAutoDownloadDescription"
android:paddingVertical="1dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
style="@style/TextAppearance.Session.ConversationSettings.OptionSummary"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
<!-- Admin settings -->
<androidx.constraintlayout.widget.Group
android:id="@+id/adminControlsGroup"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="adminContainer,adminSettingsTitle"/>
<TextView
style="@style/TextAppearance.Session.ConversationSettings.Subtitle"
android:text="@string/adminSettings"
android:id="@+id/adminSettingsTitle"
app:layout_constraintTop_toBottomOf="@+id/mainConversationSettingContainer"
android:layout_marginTop="@dimen/medium_spacing"
app:layout_constraintStart_toStartOf="@+id/adminContainer"
app:layout_constraintEnd_toEndOf="@+id/adminContainer"
app:layout_constraintHorizontal_bias="0"
android:layout_marginHorizontal="@dimen/large_spacing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<LinearLayout
android:id="@+id/adminContainer"
android:background="@drawable/preference_single_no_padding"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/adminSettingsTitle"
android:layout_marginTop="@dimen/small_spacing"
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@drawable/debug_border"
android:orientation="vertical">
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_edit_group"
style="@style/TextAppearance.Session.ConversationSettings.Option"
android:text="@string/groupEdit"
android:id="@+id/editGroup"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/editGroupDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<include
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<LinearLayout
android:background="?selectableItemBackground"
android:id="@+id/disappearingMessages"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:paddingHorizontal="@dimen/very_large_spacing"
android:orientation="horizontal">
<ImageView
android:src="@drawable/ic_disappearing_messages"
android:layout_gravity="center"
android:layout_width="@dimen/setting_image_size"
android:layout_height="@dimen/setting_image_size"
app:tint="?android:textColorPrimary" />
<LinearLayout
android:layout_marginStart="@dimen/very_large_spacing"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/disappearingMessages"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:paddingVertical="1dp"
android:layout_gravity="center"
style="@style/TextAppearance.Session.ConversationSettings.Option"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/disappearingMessagesValue"
tools:text="@tools:sample/lorem"
android:paddingVertical="1dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
style="@style/TextAppearance.Session.ConversationSettings.OptionSummary"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/destructiveContainer"
android:background="@drawable/preference_single_no_padding"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/adminContainer"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginBottom="@dimen/massive_spacing"
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@drawable/debug_border"
android:orientation="vertical">
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_clear_messages"
style="@style/TextAppearance.Session.ConversationSettings.Option.Destructive"
android:text="@string/clearMessages"
android:id="@+id/clearMessages"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/clearMessagesDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_leave_group"
style="@style/TextAppearance.Session.ConversationSettings.Option.Destructive"
android:text="@string/groupLeave"
android:id="@+id/leaveGroup"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
<include
android:id="@+id/leaveGroupDivider"
android:layout_marginHorizontal="@dimen/large_spacing"
layout="@layout/preference_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:background="?selectableItemBackground"
app:drawableStartCompat="@drawable/ic_delete"
style="@style/TextAppearance.Session.ConversationSettings.Option.Destructive"
android:text="@string/groupDelete"
android:id="@+id/deleteGroup"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_leave_group"
android:contentDescription="@string/AccessibilityId_groupLeave"
android:title="@string/groupLeave"
app:showAsAction="collapseActionView"/>
</menu>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_edit_group"
android:contentDescription="@string/AccessibilityId_groupEdit"
android:title="@string/groupEdit"
app:showAsAction="collapseActionView" />
</menu>