refactor: testing and building out new edit closed group capabilities
This commit is contained in:
parent
e67946146c
commit
d8bc838754
|
@ -18,6 +18,7 @@ plugins {
|
|||
id 'kotlin-kapt'
|
||||
id 'com.google.dagger.hilt.android'
|
||||
id 'com.google.devtools.ksp' version "$kspVersion"
|
||||
id 'app.cash.molecule'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
|
|
@ -152,6 +152,10 @@
|
|||
android:theme="@style/Theme.Session.DayNight.FlatActionBar"
|
||||
android:label="@string/blocked_contacts_title"
|
||||
/>
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.groups.EditLegacyClosedGroupActivity"
|
||||
android:label="@string/activity_edit_closed_group_title"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
||||
android:label="@string/activity_edit_closed_group_title"
|
||||
|
|
|
@ -14,8 +14,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
|||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.database.LokiThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.groups.EditLegacyClosedGroupActivity
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -164,9 +163,9 @@ class ConversationSettingsActivity: PassphraseRequiredActionBarActivity(), View.
|
|||
v === binding.editGroup -> {
|
||||
val recipient = viewModel.recipient ?: return
|
||||
if (!recipient.isClosedGroupRecipient || !recipient.isLegacyClosedGroupRecipient) return
|
||||
val intent = Intent(this, EditClosedGroupActivity::class.java)
|
||||
val intent = Intent(this, EditLegacyClosedGroupActivity::class.java)
|
||||
val groupID: String = recipient.address.toGroupString()
|
||||
intent.putExtra(EditClosedGroupActivity.groupIDKey, groupID)
|
||||
intent.putExtra(EditLegacyClosedGroupActivity.groupIDKey, groupID)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ 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.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
|
||||
import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
|
||||
import org.thoughtcrime.securesms.groups.EditLegacyClosedGroupActivity
|
||||
import org.thoughtcrime.securesms.groups.EditLegacyClosedGroupActivity.Companion.groupIDKey
|
||||
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
|
||||
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
|
@ -281,7 +281,7 @@ object ConversationMenuHelper {
|
|||
|
||||
private fun editClosedGroup(context: Context, thread: Recipient) {
|
||||
if (!thread.isLegacyClosedGroupRecipient) { return }
|
||||
val intent = Intent(context, EditClosedGroupActivity::class.java)
|
||||
val intent = Intent(context, EditLegacyClosedGroupActivity::class.java)
|
||||
val groupID: String = thread.address.toGroupString()
|
||||
intent.putExtra(groupIDKey, groupID)
|
||||
context.startActivity(intent)
|
||||
|
|
|
@ -1251,6 +1251,9 @@ open class Storage(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getLibSessionClosedGroup(groupSessionId: String) =
|
||||
configFactory.userGroups?.getClosedGroup(groupSessionId)
|
||||
|
||||
override fun setServerCapabilities(server: String, capabilities: List<String>) {
|
||||
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
|
||||
}
|
||||
|
|
|
@ -1,342 +1,30 @@
|
|||
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.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.navigation.dependency
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
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.Log
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import org.thoughtcrime.securesms.groups.compose.EditGroupViewModel
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditClosedGroupActivity: PassphraseRequiredActionBarActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var groupConfigFactory: ConfigFactory
|
||||
@Inject
|
||||
lateinit var storage: Storage
|
||||
|
||||
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
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
setContent {
|
||||
AppTheme {
|
||||
DestinationsNavHost(
|
||||
navGraph = NavGraphs.editGroup,
|
||||
dependenciesContainerBuilder = {
|
||||
dependency(NavGraphs.editGroup) {
|
||||
EditGroupViewModel.Factory()
|
||||
}
|
||||
}
|
||||
|
||||
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!!, false)
|
||||
} 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
|
||||
updateGroupConfig()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGroupConfig() {
|
||||
val latestRecipient = storage.getRecipientSettings(Address.fromSerialized(groupID))
|
||||
?: return Log.w("Loki", "No recipient settings when trying to update group config")
|
||||
val latestGroup = storage.getGroup(groupID)
|
||||
?: return Log.w("Loki", "No group record when trying to update group config")
|
||||
groupConfigFactory.updateLegacyGroup(latestRecipient, latestGroup)
|
||||
}
|
||||
|
||||
class GroupMembers(val members: List<String>, val zombieMembers: List<String>)
|
||||
}
|
|
@ -4,13 +4,13 @@ import android.content.Context
|
|||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader
|
||||
|
||||
class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<EditClosedGroupActivity.GroupMembers>(context) {
|
||||
class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader<EditLegacyClosedGroupActivity.GroupMembers>(context) {
|
||||
|
||||
override fun loadInBackground(): EditClosedGroupActivity.GroupMembers {
|
||||
override fun loadInBackground(): EditLegacyClosedGroupActivity.GroupMembers {
|
||||
val groupDatabase = DatabaseComponent.get(context).groupDatabase()
|
||||
val members = groupDatabase.getGroupMembers(groupID, true)
|
||||
val zombieMembers = groupDatabase.getGroupZombieMembers(groupID)
|
||||
return EditClosedGroupActivity.GroupMembers(
|
||||
return EditLegacyClosedGroupActivity.GroupMembers(
|
||||
members.map {
|
||||
it.address.toString()
|
||||
},
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
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.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
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.Log
|
||||
import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsActivity
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EditLegacyClosedGroupActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var groupConfigFactory: ConfigFactory
|
||||
@Inject
|
||||
lateinit var storage: Storage
|
||||
|
||||
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@EditLegacyClosedGroupActivity)
|
||||
}
|
||||
|
||||
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@EditLegacyClosedGroupActivity, 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@EditLegacyClosedGroupActivity).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@EditLegacyClosedGroupActivity, 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@EditLegacyClosedGroupActivity, 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!!, false)
|
||||
} 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
|
||||
updateGroupConfig()
|
||||
finish()
|
||||
}.failUi { exception ->
|
||||
val message = if (exception is MessageSender.Error) exception.description else "An error occurred"
|
||||
Toast.makeText(this@EditLegacyClosedGroupActivity, message, Toast.LENGTH_LONG).show()
|
||||
loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGroupConfig() {
|
||||
val latestRecipient = storage.getRecipientSettings(Address.fromSerialized(groupID))
|
||||
?: return Log.w("Loki", "No recipient settings when trying to update group config")
|
||||
val latestGroup = storage.getGroup(groupID)
|
||||
?: return Log.w("Loki", "No group record when trying to update group config")
|
||||
groupConfigFactory.updateLegacyGroup(latestRecipient, latestGroup)
|
||||
}
|
||||
|
||||
class GroupMembers(val members: List<String>, val zombieMembers: List<String>)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package org.thoughtcrime.securesms.groups.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.RecompositionMode.Immediate
|
||||
import app.cash.molecule.launchMolecule
|
||||
import com.google.android.gms.auth.api.signin.internal.Storage
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import org.session.libsession.database.StorageProtocol
|
||||
import javax.inject.Inject
|
||||
|
||||
@EditGroupNavGraph(start = true)
|
||||
@Composable
|
||||
@Destination
|
||||
fun EditClosedGroupScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: EditGroupViewModel
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class EditGroupViewModel @Inject constructor(private val groupSessionId: String,
|
||||
private val storage: StorageProtocol): ViewModel() {
|
||||
|
||||
val viewState = viewModelScope.launchMolecule(Immediate) {
|
||||
|
||||
val closedGroup = remember {
|
||||
// storage.getLibSessionClosedGroup()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class EditGroupState(
|
||||
val viewState: EditGroupViewState,
|
||||
val eventSink: (Unit)->Unit
|
||||
)
|
||||
|
||||
sealed class EditGroupViewState {
|
||||
data object NoOp: EditGroupViewState()
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.groups.compose
|
||||
|
||||
import com.ramcosta.composedestinations.annotation.NavGraph
|
||||
|
||||
@NavGraph
|
||||
annotation class EditGroupNavGraph(
|
||||
val start: Boolean = false
|
||||
)
|
|
@ -4,7 +4,7 @@
|
|||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context="org.thoughtcrime.securesms.groups.EditClosedGroupActivity">
|
||||
tools:context="org.thoughtcrime.securesms.groups.EditLegacyClosedGroupActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mainContentContainer"
|
||||
|
|
|
@ -17,6 +17,7 @@ buildscript {
|
|||
classpath "com.google.gms:google-services:$googleServicesVersion"
|
||||
classpath "com.squareup:javapoet:1.13.0"
|
||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion"
|
||||
classpath 'app.cash.molecule:molecule-gradle-plugin:1.2.1'
|
||||
if (project.hasProperty('huawei')) classpath 'com.huawei.agconnect:agcp:1.9.1.300'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package org.session.libsession.database
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import network.loki.messenger.libsession_util.Config
|
||||
import network.loki.messenger.libsession_util.util.Conversation
|
||||
import network.loki.messenger.libsession_util.util.GroupInfo
|
||||
import org.session.libsession.messaging.BlindedIdMapping
|
||||
import org.session.libsession.messaging.calls.CallMessageType
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
|
@ -32,6 +34,7 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
|
|||
import org.session.libsignal.crypto.ecc.ECKeyPair
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
import org.session.libsignal.messages.SignalServiceGroup
|
||||
import org.session.libsignal.protos.SignalServiceProtos.ConfigurationMessage.ClosedGroup
|
||||
import org.session.libsignal.utilities.SessionId
|
||||
import org.session.libsignal.utilities.guava.Optional
|
||||
import network.loki.messenger.libsession_util.util.Contact as LibSessionContact
|
||||
|
@ -161,6 +164,7 @@ interface StorageProtocol {
|
|||
fun getMembers(groupPublicKey: String): List<LibSessionGroupMember>
|
||||
fun acceptClosedGroupInvite(groupId: SessionId, name: String, authData: ByteArray, invitingAdmin: SessionId)
|
||||
fun setGroupInviteCompleteIfNeeded(approved: Boolean, invitee: String, closedGroup: SessionId)
|
||||
fun getLibSessionClosedGroup(groupSessionId: String): GroupInfo.ClosedGroupInfo?
|
||||
|
||||
// Groups
|
||||
fun getAllGroups(includeInactive: Boolean): List<GroupRecord>
|
||||
|
|
Loading…
Reference in New Issue