diff --git a/app/build.gradle b/app/build.gradle index 15546840f..393994bb3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 608069604..9668b0d83 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -152,6 +152,10 @@ android:theme="@style/Theme.Session.DayNight.FlatActionBar" android:label="@string/blocked_contacts_title" /> + { 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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index 5dcfc8afd..b855e411a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index a3b4ca64a..31f153d86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -1251,6 +1251,9 @@ open class Storage( } } + override fun getLibSessionClosedGroup(groupSessionId: String) = + configFactory.userGroups?.getClosedGroup(groupSessionId) + override fun setServerCapabilities(server: String, capabilities: List) { return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt index 9fee8adaf..753656cc7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt @@ -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() { +class EditClosedGroupActivity: PassphraseRequiredActionBarActivity() { - @Inject - lateinit var groupConfigFactory: ConfigFactory - @Inject - lateinit var storage: Storage - - private val originalMembers = HashSet() - private val zombies = HashSet() - private val members = HashSet() - private val allMembers: Set - 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(R.id.addMembersClosedGroupButton).setOnClickListener { - onAddMembersClick() - } - - findViewById(R.id.rvUserList).apply { - adapter = memberListAdapter - layoutManager = LinearLayoutManager(this@EditClosedGroupActivity) - } - - lblGroupNameDisplay.text = originalName - cntGroupNameDisplay.setOnClickListener { isEditingName = true } - findViewById(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false } - findViewById(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 { - - override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { - return EditClosedGroupLoader(this@EditClosedGroupActivity, groupID) - } - - override fun onLoadFinished(loader: Loader, 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) { - 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 = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) { - MessageSender.explicitLeave(groupPublicKey!!, false) - } else { - task { - if (hasNameChanged) { - MessageSender.explicitNameChange(groupPublicKey!!, name) + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + setContent { + AppTheme { + DestinationsNavHost( + navGraph = NavGraphs.editGroup, + dependenciesContainerBuilder = { + dependency(NavGraphs.editGroup) { + EditGroupViewModel.Factory() + } } - 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, val zombieMembers: List) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupLoader.kt index b1e0b5e1d..2acdfcd78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupLoader.kt @@ -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(context) { +class EditClosedGroupLoader(context: Context, val groupID: String) : AsyncLoader(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() }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyClosedGroupActivity.kt new file mode 100644 index 000000000..fb572f0f0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyClosedGroupActivity.kt @@ -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() + private val zombies = HashSet() + private val members = HashSet() + private val allMembers: Set + 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(R.id.addMembersClosedGroupButton).setOnClickListener { + onAddMembersClick() + } + + findViewById(R.id.rvUserList).apply { + adapter = memberListAdapter + layoutManager = LinearLayoutManager(this@EditLegacyClosedGroupActivity) + } + + lblGroupNameDisplay.text = originalName + cntGroupNameDisplay.setOnClickListener { isEditingName = true } + findViewById(R.id.btnCancelGroupNameEdit).setOnClickListener { isEditingName = false } + findViewById(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 { + + override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { + return EditClosedGroupLoader(this@EditLegacyClosedGroupActivity, groupID) + } + + override fun onLoadFinished(loader: Loader, 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) { + 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 = 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, val zombieMembers: List) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt new file mode 100644 index 000000000..51ec75eb2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupNavGraph.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupNavGraph.kt new file mode 100644 index 000000000..b3cd75fee --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroupNavGraph.kt @@ -0,0 +1,8 @@ +package org.thoughtcrime.securesms.groups.compose + +import com.ramcosta.composedestinations.annotation.NavGraph + +@NavGraph +annotation class EditGroupNavGraph( + val start: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/res/layout/activity_edit_closed_group.xml b/app/src/main/res/layout/activity_edit_closed_group.xml index e8a892f18..298bdfcbd 100644 --- a/app/src/main/res/layout/activity_edit_closed_group.xml +++ b/app/src/main/res/layout/activity_edit_closed_group.xml @@ -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"> 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