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

318 lines
13 KiB
Kotlin

package org.thoughtcrime.securesms.groups
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
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
import kotlinx.android.synthetic.main.activity_settings.*
import network.loki.messenger.R
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut
import java.io.IOException
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private val originalMembers = HashSet<String>()
private val zombies = HashSet<String>()
private val members = HashSet<String>()
private val allMembers: Set<String>
get() {
return members + zombies
}
private var hasNameChanged = false
private var isSelfAdmin = false
private var isLoading = false
set(newValue) { field = newValue; invalidateOptionsMenu() }
private lateinit var groupID: String
private lateinit var originalName: String
private lateinit var name: String
private var isEditingName = false
set(value) {
if (field == value) return
field = value
handleIsEditingNameChanged()
}
private val memberListAdapter by lazy {
if (isSelfAdmin)
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin, this::onMemberClick)
else
EditClosedGroupMembersAdapter(this, GlideApp.with(this), isSelfAdmin)
}
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
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)
setContentView(R.layout.activity_edit_closed_group)
supportActionBar!!.setHomeAsUpIndicator(
ThemeUtil.getThemedDrawableResId(this, R.attr.actionModeCloseDrawable))
groupID = intent.getStringExtra(groupIDKey)!!
val groupInfo = DatabaseComponent.get(this).groupDatabase().getGroup(groupID).get()
originalName = groupInfo.title
isSelfAdmin = groupInfo.admins.any{ it.serialize() == TextSecurePreferences.getLocalNumber(this) }
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)
findViewById<View>(R.id.addMembersClosedGroupButton).setOnClickListener {
onAddMembersClick()
}
findViewById<RecyclerView>(R.id.rvUserList).apply {
adapter = memberListAdapter
layoutManager = LinearLayoutManager(this@EditClosedGroupActivity)
}
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 -> {
saveName()
return@setOnEditorActionListener true
}
else -> return@setOnEditorActionListener false
}
}
LoaderManager.getInstance(this).initLoader(loaderID, null, object : LoaderManager.LoaderCallbacks<GroupMembers> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<GroupMembers> {
return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID)
}
override fun onLoadFinished(loader: Loader<GroupMembers>, groupMembers: GroupMembers) {
// We no longer need any subsequent loading events
// (they will occur on every activity resume).
LoaderManager.getInstance(this@EditClosedGroupActivity).destroyLoader(loaderID)
members.clear()
members.addAll(groupMembers.members.toHashSet())
zombies.clear()
zombies.addAll(groupMembers.zombieMembers.toHashSet())
originalMembers.clear()
originalMembers.addAll(members + zombies)
updateMembers()
}
override fun onLoaderReset(loader: Loader<GroupMembers>) {
updateMembers()
}
})
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_edit_closed_group, menu)
return allMembers.isNotEmpty() && !isLoading
}
// endregion
// region Updating
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
addUsersRequestCode -> {
if (resultCode != RESULT_OK) return
if (data == null || data.extras == null || !data.hasExtra(SelectContactsActivity.selectedContactsKey)) return
val selectedContacts = data.extras!!.getStringArray(SelectContactsActivity.selectedContactsKey)!!.toSet()
members.addAll(selectedContacts)
updateMembers()
}
}
}
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)
}
}
private fun updateMembers() {
memberListAdapter.setMembers(allMembers)
memberListAdapter.setZombieMembers(zombies)
mainContentContainer.visibility = if (allMembers.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (allMembers.isEmpty()) View.VISIBLE else View.GONE
invalidateOptionsMenu()
}
// endregion
// region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_apply -> if (!isLoading) { commitChanges() }
}
return super.onOptionsItemSelected(item)
}
private fun onMemberClick(member: String) {
val bottomSheet = ClosedGroupEditingOptionsBottomSheet()
bottomSheet.onRemoveTapped = {
if (zombies.contains(member)) zombies.remove(member)
else members.remove(member)
updateMembers()
bottomSheet.dismiss()
}
bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet")
}
private fun onAddMembersClick() {
val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java)
intent.putExtra(SelectContactsActivity.usersToExcludeKey, allMembers.toTypedArray())
intent.putExtra(SelectContactsActivity.emptyStateTextKey, "No contacts to add")
startActivityForResult(intent, addUsersRequestCode)
}
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()
}
if (name.length >= 64) {
return Toast.makeText(this, R.string.activity_edit_closed_group_group_name_too_long_error, Toast.LENGTH_SHORT).show()
}
this.name = name
lblGroupNameDisplay.text = name
hasNameChanged = true
isEditingName = false
}
private fun commitChanges() {
val hasMemberListChanges = (allMembers != originalMembers)
if (!hasNameChanged && !hasMemberListChanges) {
return finish()
}
val name = if (hasNameChanged) this.name else originalName
val members = this.allMembers.map {
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
val originalMembers = this.originalMembers.map {
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
var isClosedGroup: Boolean
var groupPublicKey: String?
try {
groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
isClosedGroup = DatabaseComponent.get(this).lokiAPIDatabase().isClosedGroup(groupPublicKey)
} catch (e: IOException) {
groupPublicKey = null
isClosedGroup = false
}
if (members.isEmpty()) {
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
}
val maxGroupMembers = if (isClosedGroup) groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) {
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
val userAsRecipient = Recipient.from(this, Address.fromSerialized(userPublicKey), false)
if (!members.contains(userAsRecipient) && !members.map { it.address.toString() }.containsAll(originalMembers.minus(userPublicKey))) {
val message = "Can't leave while adding or removing other members."
return Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
}
if (isClosedGroup) {
isLoading = true
loaderContainer.fadeIn()
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
MessageSender.explicitLeave(groupPublicKey!!, true)
} else {
task {
if (hasNameChanged) {
MessageSender.explicitNameChange(groupPublicKey!!, name)
}
members.filterNot { it in originalMembers }.let { adds ->
if (adds.isNotEmpty()) MessageSender.explicitAddMembers(groupPublicKey!!, adds.map { it.address.serialize() })
}
originalMembers.filterNot { it in members }.let { removes ->
if (removes.isNotEmpty()) MessageSender.explicitRemoveMembers(groupPublicKey!!, removes.map { it.address.serialize() })
}
}
}
promise.successUi {
loaderContainer.fadeOut()
isLoading = false
finish()
}.failUi { exception ->
val message = if (exception is MessageSender.Error) exception.description else "An error occurred"
Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
loaderContainer.fadeOut()
isLoading = false
}
}
}
class GroupMembers(val members: List<String>, val zombieMembers: List<String>) { }
}