session-android/app/src/main/java/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt

306 lines
13 KiB
Kotlin
Raw Normal View History

package org.thoughtcrime.securesms.loki.activities
2020-06-09 07:03:43 +02:00
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
2020-06-09 07:03:43 +02:00
import android.view.inputmethod.InputMethodManager
import android.widget.*
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
2021-01-21 05:42:43 +01:00
import kotlinx.android.synthetic.main.activity_settings.*
import network.loki.messenger.R
2021-01-13 06:13:49 +01:00
import nl.komponents.kovenant.Promise
2021-02-02 07:05:21 +01:00
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
2021-01-13 07:11:30 +01:00
import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
2020-08-18 01:34:22 +02:00
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
2021-01-13 06:13:49 +01:00
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
2021-01-13 07:11:30 +01:00
import org.session.libsession.messaging.threads.recipients.Recipient
2021-02-17 06:09:36 +01:00
import org.session.libsession.utilities.GroupUtil
2021-01-15 05:36:30 +01:00
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil
2021-02-17 06:09:36 +01:00
import org.session.libsignal.service.loki.utilities.toHexString
import java.io.IOException
2020-08-14 06:52:15 +02:00
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private val originalMembers = HashSet<String>()
private val members = HashSet<String>()
2020-08-18 01:34:22 +02:00
private var hasNameChanged = false
private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() }
2020-08-14 06:52:15 +02:00
private lateinit var groupID: String
2020-08-14 06:52:15 +02:00
private lateinit var originalName: String
2020-08-18 01:34:22 +02:00
private lateinit var name: String
2020-08-18 01:34:22 +02:00
private var isEditingName = false
set(value) {
if (field == value) return
field = value
2020-08-18 01:34:22 +02:00
handleIsEditingNameChanged()
}
2020-08-18 01:34:22 +02:00
private val memberListAdapter by lazy {
EditClosedGroupMembersAdapter(this, GlideApp.with(this), this::onMemberClick)
}
private lateinit var mainContentContainer: LinearLayout
private lateinit var cntGroupNameEdit: LinearLayout
private lateinit var cntGroupNameDisplay: LinearLayout
private lateinit var edtGroupName: EditText
private lateinit var emptyStateContainer: LinearLayout
private lateinit var lblGroupNameDisplay: TextView
private lateinit var loaderContainer: View
2020-08-18 01:34:22 +02:00
companion object {
@JvmStatic val groupIDKey = "groupIDKey"
private val loaderID = 0
val addUsersRequestCode = 124
val legacyGroupSizeLimit = 10
}
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
2020-08-18 01:34:22 +02:00
setContentView(R.layout.activity_edit_closed_group)
2020-08-14 06:52:15 +02:00
supportActionBar!!.setHomeAsUpIndicator(
ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable))
groupID = intent.getStringExtra(groupIDKey)!!
2020-08-18 01:34:22 +02:00
originalName = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().title
name = originalName
mainContentContainer = findViewById(R.id.mainContentContainer)
cntGroupNameEdit = findViewById(R.id.cntGroupNameEdit)
cntGroupNameDisplay = findViewById(R.id.cntGroupNameDisplay)
edtGroupName = findViewById(R.id.edtGroupName)
emptyStateContainer = findViewById(R.id.emptyStateContainer)
lblGroupNameDisplay = findViewById(R.id.lblGroupNameDisplay)
loaderContainer = findViewById(R.id.loaderContainer)
2020-08-14 06:52:15 +02:00
findViewById<View>(R.id.addMembersClosedGroupButton).setOnClickListener {
onAddMembersClick()
}
findViewById<RecyclerView>(R.id.rvUserList).apply {
adapter = memberListAdapter
layoutManager = LinearLayoutManager(this@EditClosedGroupActivity)
}
2020-08-14 06:52:15 +02:00
2020-08-18 01:34:22 +02:00
lblGroupNameDisplay.text = originalName
cntGroupNameDisplay.setOnClickListener { isEditingName = true }
findViewById<View>(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false }
findViewById<View>(R.id.btnSaveGroupNameEdit).setOnClickListener { saveName() }
edtGroupName.setImeActionLabel(getString(R.string.save), EditorInfo.IME_ACTION_DONE)
edtGroupName.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE -> {
2020-08-18 01:34:22 +02:00
saveName()
return@setOnEditorActionListener true
}
else -> return@setOnEditorActionListener false
}
}
LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<List<String>> {
2020-08-18 01:34:22 +02:00
2020-08-14 06:52:15 +02:00
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> {
return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID)
2020-08-14 06:52:15 +02:00
}
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) {
// We no longer need any subsequent loading events
// (they will occur on every activity resume).
LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID)
2020-08-14 06:52:15 +02:00
originalMembers.clear()
originalMembers.addAll(members.toHashSet())
updateMembers(originalMembers)
}
override fun onLoaderReset(loader: Loader<List<String>>) {
updateMembers(setOf())
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_edit_closed_group, menu)
return members.isNotEmpty() && !isLoading
}
// endregion
// region Updating
2020-08-18 01:34:22 +02:00
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
addUsersRequestCode -> {
2020-08-18 01:34:22 +02:00
if (resultCode != RESULT_OK) return
if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return
val selectedContacts = data.extras!!.getStringArray(SelectContactsActivity.selectedContactsKey)!!.toSet()
val changedMembers = members + selectedContacts
updateMembers(changedMembers)
}
}
}
private fun handleIsEditingNameChanged() {
cntGroupNameEdit.visibility = if (isEditingName) View.VISIBLE else View.INVISIBLE
cntGroupNameDisplay.visibility = if (isEditingName) View.INVISIBLE else View.VISIBLE
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (isEditingName) {
edtGroupName.setText(name)
edtGroupName.selectAll()
edtGroupName.requestFocus()
inputMethodManager.showSoftInput(edtGroupName, 0)
} else {
inputMethodManager.hideSoftInputFromWindow(edtGroupName.windowToken, 0)
}
}
2020-08-14 06:52:15 +02:00
private fun updateMembers(members: Set<String>) {
this.members.clear()
this.members.addAll(members)
2020-08-18 01:34:22 +02:00
memberListAdapter.setMembers(members)
2021-01-15 05:36:30 +01:00
val admins = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().admins.map { it.toString() }.toMutableSet()
2021-01-13 06:13:49 +01:00
admins.remove(TextSecurePreferences.getLocalNumber(this))
memberListAdapter.setLockedMembers(admins)
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
2020-08-18 01:34:22 +02:00
invalidateOptionsMenu()
}
// endregion
// region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_apply -> if (!isLoading) { commitChanges() }
}
return super.onOptionsItemSelected(item)
}
2020-08-14 06:52:15 +02:00
private fun onMemberClick(member: String) {
2020-08-18 01:34:22 +02:00
val bottomSheet = ClosedGroupEditingOptionsBottomSheet()
bottomSheet.onRemoveTapped = {
2020-08-14 06:52:15 +02:00
val changedMembers = members - member
updateMembers(changedMembers)
bottomSheet.dismiss()
}
2020-08-18 01:34:22 +02:00
bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet")
}
2020-08-14 06:52:15 +02:00
private fun onAddMembersClick() {
val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java)
intent.putExtra(SelectContactsActivity.usersToExcludeKey, members.toTypedArray())
2020-10-02 07:24:13 +02:00
intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add")
startActivityForResult(intent, addUsersRequestCode)
}
2020-08-18 01:34:22 +02:00
private fun saveName() {
val name = edtGroupName.text.toString().trim()
if (name.isEmpty()) {
return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_missing_error, Toast.LENGTH_SHORT).show()
}
2020-08-18 01:34:22 +02:00
if (name.length >= 64) {
return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_too_long_error, Toast.LENGTH_SHORT).show()
}
2020-08-18 01:34:22 +02:00
this.name = name
lblGroupNameDisplay.text = name
hasNameChanged = true
isEditingName = false
}
2020-08-18 01:34:22 +02:00
private fun commitChanges() {
2021-01-13 06:13:49 +01:00
val hasMemberListChanges = (members != originalMembers)
2020-08-14 06:52:15 +02:00
2020-08-18 01:34:22 +02:00
if (!hasNameChanged && !hasMemberListChanges) {
return finish()
}
2020-08-14 06:52:15 +02:00
2020-08-18 01:34:22 +02:00
val name = if (hasNameChanged) this.name else originalName
2020-08-14 06:52:15 +02:00
2020-08-18 01:34:22 +02:00
val members = this.members.map {
2020-08-14 06:52:15 +02:00
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
2021-02-02 07:05:21 +01:00
val originalMembers = this.originalMembers.map {
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
2020-08-18 01:34:22 +02:00
val admins = members.toSet() //TODO For now, consider all the users to be admins.
2021-02-18 04:14:05 +01:00
var isClosedGroup: Boolean
var groupPublicKey: String?
try {
2021-02-17 06:09:36 +01:00
groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
2021-02-18 04:14:05 +01:00
isClosedGroup = DatabaseFactory.getLokiAPIDatabase(this).isClosedGroup(groupPublicKey)
} catch (e: IOException) {
groupPublicKey = null
2021-02-18 04:14:05 +01:00
isClosedGroup = false
}
2021-01-27 05:54:12 +01:00
if (members.isEmpty()) {
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
}
2020-08-18 01:34:22 +02:00
2021-02-18 04:14:05 +01:00
val maxGroupMembers = if (isClosedGroup) ClosedGroupsProtocolV2.groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) {
2021-01-27 05:54:12 +01:00
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
2021-01-15 05:36:30 +01:00
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
2021-01-13 06:13:49 +01:00
val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false)
2021-01-15 05:36:30 +01:00
if (!members.contains(userAsRecipient) && !members.map { it.address.toString() }.containsAll(originalMembers.minus(userPublicKey))) {
2021-01-13 06:13:49 +01:00
val message = "Can't leave while adding or removing other members."
return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
}
2021-02-18 04:14:05 +01:00
if (isClosedGroup) {
isLoading = true
loaderContainer.fadeIn()
2021-02-02 07:05:21 +01:00
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
2021-02-11 06:34:01 +01:00
ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!)
2021-01-13 06:13:49 +01:00
} else {
2021-02-11 06:34:01 +01:00
task {
if (hasNameChanged) {
ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity, groupPublicKey!!, name)
}
2021-02-11 06:34:01 +01:00
members.filterNot { it in originalMembers }.let { adds ->
if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() })
}
2021-02-11 06:34:01 +01:00
originalMembers.filterNot { it in members }.let { removes ->
if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() })
}
2021-02-11 06:34:01 +01:00
}
2021-01-13 06:13:49 +01:00
}
promise.successUi {
loaderContainer.fadeOut()
isLoading = false
finish()
}.failUi { exception ->
2021-02-17 06:09:36 +01:00
val message = if (exception is ClosedGroupsProtocolV2.Error) exception.description else "An error occurred"
Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
loaderContainer.fadeOut()
isLoading = false
}
}
}
}