You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-android/src/org/thoughtcrime/securesms/loki/activities/EditClosedGroupActivity.kt

264 lines
11 KiB
Kotlin

package org.thoughtcrime.securesms.loki.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
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.Toast
import androidx.appcompat.content.res.AppCompatResources
import kotlinx.android.synthetic.main.activity_create_closed_group.*
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
import kotlinx.android.synthetic.main.activity_edit_closed_group.*
import kotlinx.android.synthetic.main.activity_edit_closed_group.loader
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
import network.loki.messenger.R
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.dialogs.ClosedGroupEditingOptionsBottomSheet
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ThemeUtil
import org.whispersystems.signalservice.loki.utilities.toHexString
import java.io.IOException
class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
private val originalMembers = HashSet<String>()
private val members = HashSet<String>()
private var hasNameChanged = 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 {
EditClosedGroupMembersAdapter(this, GlideApp.with(this), this::onMemberClick)
}
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)!!
originalName = DatabaseFactory.getGroupDatabase(this).getGroup(groupID).get().title
name = originalName
addMembersClosedGroupButton.setOnClickListener { onAddMembersClick() }
recyclerView.adapter = memberListAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
lblGroupNameDisplay.text = originalName
cntGroupNameDisplay.setOnClickListener { isEditingName = true }
btnCancelGroupNameEdit.setOnClickListener { isEditingName = false }
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<List<String>> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> {
return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID)
}
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)
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
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()
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)
}
}
private fun updateMembers(members: Set<String>) {
this.members.clear()
this.members.addAll(members)
memberListAdapter.setMembers(members)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
memberListAdapter.setLockedMembers(arrayListOf(userPublicKey))
mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (members.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 = {
val changedMembers = members - member
updateMembers(changedMembers)
bottomSheet.dismiss()
}
bottomSheet.show(supportFragmentManager, "GroupEditingOptionsBottomSheet")
}
private fun onAddMembersClick() {
val intent = Intent(this@EditClosedGroupActivity, SelectContactsActivity::class.java)
intent.putExtra(SelectContactsActivity.usersToExcludeKey, members.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 = members != originalMembers
if (!hasNameChanged && !hasMemberListChanges) {
return finish()
}
val name = if (hasNameChanged) this.name else originalName
val members = this.members.map {
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
val admins = members.toSet() //TODO For now, consider all the users to be admins.
var isSSKBasedClosedGroup: Boolean
var groupPublicKey: String?
try {
groupPublicKey = ClosedGroupsProtocol.doubleDecodeGroupID(groupID).toHexString()
isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey)
} catch (e: IOException) {
groupPublicKey = null
isSSKBasedClosedGroup = false
}
if (members.size < 1) {
return Toast.makeText(this, R.string.activity_edit_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
}
val maxGroupMembers = if (isSSKBasedClosedGroup) ClosedGroupsProtocol.groupSizeLimit else legacyGroupSizeLimit
if (members.size >= maxGroupMembers) {
// TODO: Update copy for SSK based closed groups
return Toast.makeText(this, R.string.activity_edit_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
}
if (isSSKBasedClosedGroup) {
isLoading = true
loader.fadeIn()
ClosedGroupsProtocol.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name).successUi {
loader.fadeOut()
isLoading = false
finish()
}.failUi { exception ->
val message = if (exception is ClosedGroupsProtocol.Error) exception.description else "An error occurred"
Toast.makeText(this@EditClosedGroupActivity, message, Toast.LENGTH_LONG).show()
isLoading = false
}
} else {
GroupManager.updateGroup(this, groupID, members, null, name, admins)
}
}
}