fix: view model should give and receive better structured events and state
This commit is contained in:
parent
baf2157331
commit
fabe2d44da
|
@ -1,6 +1,5 @@
|
||||||
package org.thoughtcrime.securesms.groups
|
package org.thoughtcrime.securesms.groups
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -20,12 +19,9 @@ import com.ramcosta.composedestinations.navigation.dependency
|
||||||
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||||
import com.ramcosta.composedestinations.result.ResultRecipient
|
import com.ramcosta.composedestinations.result.ResultRecipient
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import network.loki.messenger.databinding.FragmentCreateGroupBinding
|
import network.loki.messenger.databinding.FragmentCreateGroupBinding
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
|
||||||
import org.thoughtcrime.securesms.groups.compose.CreateGroup
|
import org.thoughtcrime.securesms.groups.compose.CreateGroup
|
||||||
import org.thoughtcrime.securesms.groups.compose.CreateGroupNavGraph
|
import org.thoughtcrime.securesms.groups.compose.CreateGroupNavGraph
|
||||||
import org.thoughtcrime.securesms.groups.compose.SelectContacts
|
import org.thoughtcrime.securesms.groups.compose.SelectContacts
|
||||||
|
@ -63,38 +59,20 @@ class CreateGroupFragment : Fragment() {
|
||||||
@Destination
|
@Destination
|
||||||
fun CreateGroupScreen(
|
fun CreateGroupScreen(
|
||||||
navigator: DestinationsNavigator,
|
navigator: DestinationsNavigator,
|
||||||
resultSelectContact: ResultRecipient<SelectContactScreenDestination, Contact?>,
|
resultSelectContact: ResultRecipient<SelectContactScreenDestination, Contact>,
|
||||||
viewModel: CreateGroupViewModel = hiltViewModel(),
|
viewModel: CreateGroupViewModel = hiltViewModel(),
|
||||||
getDelegate: () -> NewConversationDelegate
|
getDelegate: () -> NewConversationDelegate
|
||||||
) {
|
) {
|
||||||
val viewState by viewModel.viewState.observeAsState(ViewState.DEFAULT)
|
val viewState by viewModel.viewState.observeAsState(ViewState.DEFAULT)
|
||||||
val lifecycleScope = rememberCoroutineScope()
|
val lifecycleScope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val currentGroupState = viewModel.createGroupState
|
|
||||||
|
|
||||||
CreateGroup(
|
CreateGroup(
|
||||||
viewState,
|
viewState,
|
||||||
currentGroupState,
|
|
||||||
onCreate = { newGroup ->
|
|
||||||
// launch something to create here
|
|
||||||
// dunno if we want to key this here as a launched effect on some property :thinking:
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val groupRecipient = viewModel.tryCreateGroup(newGroup)
|
|
||||||
groupRecipient?.let { recipient ->
|
|
||||||
// launch conversation with this new group
|
|
||||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
|
||||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
|
||||||
context.startActivity(intent)
|
|
||||||
getDelegate().onDialogClosePressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSelectContact = {
|
|
||||||
navigator.navigate(SelectContactScreenDestination)
|
|
||||||
},
|
|
||||||
onClose = {
|
onClose = {
|
||||||
getDelegate().onDialogClosePressed()
|
getDelegate().onDialogClosePressed()
|
||||||
},
|
},
|
||||||
|
onSelectContact = { navigator.navigate(SelectContactScreenDestination) },
|
||||||
onBack = {
|
onBack = {
|
||||||
getDelegate().onDialogBackPressed()
|
getDelegate().onDialogBackPressed()
|
||||||
}
|
}
|
||||||
|
@ -105,17 +83,18 @@ fun CreateGroupScreen(
|
||||||
@Composable
|
@Composable
|
||||||
@Destination
|
@Destination
|
||||||
fun SelectContactScreen(
|
fun SelectContactScreen(
|
||||||
resultNavigator: ResultBackNavigator<Contact?>,
|
resultNavigator: ResultBackNavigator<Contact>,
|
||||||
viewModel: CreateGroupViewModel = hiltViewModel(),
|
viewModel: CreateGroupViewModel = hiltViewModel(),
|
||||||
getDelegate: () -> NewConversationDelegate
|
getDelegate: () -> NewConversationDelegate
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val viewState by viewModel.viewState.observeAsState(ViewState.DEFAULT)
|
||||||
|
val currentMembers = viewState.members
|
||||||
val contacts by viewModel.contacts.observeAsState(initial = emptyList())
|
val contacts by viewModel.contacts.observeAsState(initial = emptyList())
|
||||||
val currentMembers by viewModel.createGroupState.observeAsState()
|
|
||||||
|
|
||||||
SelectContacts(
|
SelectContacts(
|
||||||
contacts - currentMembers?.members.orEmpty(),
|
contacts - currentMembers,
|
||||||
onBack = { resultNavigator.navigateBack(null) },
|
onBack = { resultNavigator.navigateBack() },
|
||||||
onClose = { getDelegate().onDialogClosePressed() }
|
onClose = { getDelegate().onDialogClosePressed() }
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -6,56 +6,36 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.groups.compose.CreateGroupState
|
|
||||||
import org.thoughtcrime.securesms.groups.compose.ViewState
|
import org.thoughtcrime.securesms.groups.compose.ViewState
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class CreateGroupViewModel @Inject constructor(
|
class CreateGroupViewModel @Inject constructor(
|
||||||
private val textSecurePreferences: TextSecurePreferences,
|
|
||||||
private val storage: Storage,
|
private val storage: Storage,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _viewState = MutableLiveData(ViewState.DEFAULT)
|
private val _viewState = MutableLiveData(ViewState.DEFAULT)
|
||||||
val viewState: LiveData<ViewState> = _viewState
|
val viewState: LiveData<ViewState> = _viewState
|
||||||
|
|
||||||
val createGroupState: MutableLiveData<CreateGroupState> = MutableLiveData(CreateGroupState("","", emptySet()))
|
val contacts = liveData { emit(storage.getAllContacts()) }
|
||||||
|
|
||||||
val contacts = liveData {
|
fun tryCreateGroup(): Recipient? {
|
||||||
emit(storage.getAllContacts().toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
val currentState = _viewState.value!!
|
||||||
// viewModelScope.launch {
|
|
||||||
// threadDb.approvedConversationList.use { openCursor ->
|
|
||||||
// val reader = threadDb.readerFor(openCursor)
|
|
||||||
// val recipients = mutableListOf<Recipient>()
|
|
||||||
// while (true) {
|
|
||||||
// recipients += reader.next?.recipient ?: break
|
|
||||||
// }
|
|
||||||
// withContext(Dispatchers.Main) {
|
|
||||||
// _recipients.value = recipients
|
|
||||||
// .filter { !it.isGroupRecipient && it.hasApprovedMe() && it.address.serialize() != textSecurePreferences.getLocalNumber() }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tryCreateGroup(createGroupState: CreateGroupState): Recipient? {
|
_viewState.postValue(currentState.copy(isLoading = true, error = null))
|
||||||
_viewState.postValue(ViewState(true, null))
|
|
||||||
|
|
||||||
val name = createGroupState.groupName
|
val name = currentState.name
|
||||||
val description = createGroupState.groupDescription
|
val description = currentState.description
|
||||||
val members = createGroupState.members.toMutableSet()
|
val members = currentState.members.toMutableSet()
|
||||||
|
|
||||||
// do some validation
|
// do some validation
|
||||||
// need a name
|
// need a name
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
_viewState.postValue(
|
_viewState.postValue(
|
||||||
ViewState(false, R.string.error)
|
currentState.copy(isLoading = false, error = R.string.error)
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -66,14 +46,14 @@ class CreateGroupViewModel @Inject constructor(
|
||||||
|
|
||||||
if (members.size <= 1) {
|
if (members.size <= 1) {
|
||||||
_viewState.postValue(
|
_viewState.postValue(
|
||||||
ViewState(false, R.string.activity_create_closed_group_not_enough_group_members_error)
|
currentState.copy(isLoading = false, error = R.string.activity_create_closed_group_not_enough_group_members_error)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a group
|
// make a group
|
||||||
val newGroup = storage.createNewGroup(name, description, members)
|
val newGroup = storage.createNewGroup(name, description, members)
|
||||||
if (!newGroup.isPresent) {
|
if (!newGroup.isPresent) {
|
||||||
_viewState.postValue(ViewState(isLoading = false, null))
|
_viewState.postValue(currentState.copy(isLoading = false, error = null))
|
||||||
}
|
}
|
||||||
return newGroup.orNull()
|
return newGroup.orNull()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,6 @@ import androidx.compose.material.OutlinedButton
|
||||||
import androidx.compose.material.OutlinedTextField
|
import androidx.compose.material.OutlinedTextField
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
@ -34,9 +30,9 @@ import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import org.thoughtcrime.securesms.groups.compose.ViewState.StateUpdate
|
||||||
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
||||||
import org.thoughtcrime.securesms.ui.Divider
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
import org.thoughtcrime.securesms.ui.EditableAvatar
|
import org.thoughtcrime.securesms.ui.EditableAvatar
|
||||||
|
@ -53,24 +49,14 @@ data class CreateGroupState (
|
||||||
@Composable
|
@Composable
|
||||||
fun CreateGroup(
|
fun CreateGroup(
|
||||||
viewState: ViewState,
|
viewState: ViewState,
|
||||||
createGroupState: MutableLiveData<CreateGroupState>,
|
|
||||||
onCreate: (CreateGroupState) -> Unit,
|
|
||||||
onSelectContact: () -> Unit,
|
onSelectContact: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var name by createGroupState
|
|
||||||
var description by remember { mutableStateOf(createGroupState.groupDescription) }
|
|
||||||
var members by remember { mutableStateOf(createGroupState.members) }
|
|
||||||
|
|
||||||
val lazyState = rememberLazyListState()
|
val lazyState = rememberLazyListState()
|
||||||
|
|
||||||
val onDeleteMember = { contact: Contact ->
|
|
||||||
members -= contact
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
Column(
|
Column(
|
||||||
modifier
|
modifier
|
||||||
|
@ -93,8 +79,8 @@ fun CreateGroup(
|
||||||
// Title
|
// Title
|
||||||
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
|
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = name,
|
value = viewState.name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { viewState.updateState(StateUpdate.Name(it)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
|
@ -106,8 +92,8 @@ fun CreateGroup(
|
||||||
// Description
|
// Description
|
||||||
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
|
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = description,
|
value = viewState.description,
|
||||||
onValueChange = { description = it },
|
onValueChange = { viewState.updateState(StateUpdate.Description(it)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
|
@ -163,13 +149,15 @@ fun CreateGroup(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Group list
|
// Group list
|
||||||
memberList(contacts = members.toList(), modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp), onDeleteMember)
|
memberList(contacts = viewState.members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp)) { deletedContact ->
|
||||||
|
viewState.updateState(StateUpdate.RemoveContact(deletedContact))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Create button
|
// Create button
|
||||||
val createDescription = stringResource(id = R.string.AccessibilityId_create_closed_group_create_button)
|
val createDescription = stringResource(id = R.string.AccessibilityId_create_closed_group_create_button)
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { onCreate(CreateGroupState(name, description, members)) },
|
onClick = { viewState.create() },
|
||||||
enabled = name.isNotBlank() && !viewState.isLoading,
|
enabled = viewState.canCreate,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
|
@ -212,21 +200,38 @@ fun ClosedGroupPreview(
|
||||||
)
|
)
|
||||||
PreviewTheme(R.style.Theme_Session_DayNight_NoActionBar_Test) {
|
PreviewTheme(R.style.Theme_Session_DayNight_NoActionBar_Test) {
|
||||||
CreateGroup(
|
CreateGroup(
|
||||||
viewState = ViewState(false, null),
|
viewState = ViewState.DEFAULT.copy(
|
||||||
createGroupState = CreateGroupState("Group Name", "Test Group Description", previewMembers),
|
// override any preview parameters
|
||||||
onCreate = {},
|
),
|
||||||
onClose = {},
|
|
||||||
onSelectContact = {},
|
onSelectContact = {},
|
||||||
onBack = {},
|
onBack = {},
|
||||||
|
onClose = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ViewState(
|
data class ViewState(
|
||||||
val isLoading: Boolean,
|
val isLoading: Boolean,
|
||||||
@StringRes val error: Int?
|
@StringRes val error: Int?,
|
||||||
) {
|
val name: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val members: List<Contact> = emptyList(),
|
||||||
|
val updateState: (StateUpdate)->Unit,
|
||||||
|
val create: ()->Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val canCreate
|
||||||
|
get() = name.isNotEmpty() && members.isNotEmpty()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val DEFAULT = ViewState(false, null)
|
val DEFAULT = ViewState(false, null, updateState = {}, create = {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class StateUpdate {
|
||||||
|
data class Name(val value: String): StateUpdate()
|
||||||
|
data class Description(val value: String): StateUpdate()
|
||||||
|
data class RemoveContact(val value: Contact): StateUpdate()
|
||||||
|
data class AddContact(val value: Contact): StateUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ plugins {
|
||||||
id 'com.android.library'
|
id 'com.android.library'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlinx-serialization'
|
id 'kotlinx-serialization'
|
||||||
|
id 'kotlin-parcelize'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
@ -1,34 +1,37 @@
|
||||||
package org.session.libsession.messaging.contacts
|
package org.session.libsession.messaging.contacts
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
|
||||||
class Contact(val sessionID: String) {
|
@Parcelize
|
||||||
|
class Contact(
|
||||||
|
val sessionID: String,
|
||||||
/**
|
/**
|
||||||
* The URL from which to fetch the contact's profile picture.
|
* The URL from which to fetch the contact's profile picture.
|
||||||
*/
|
*/
|
||||||
var profilePictureURL: String? = null
|
var profilePictureURL: String? = null,
|
||||||
/**
|
/**
|
||||||
* The file name of the contact's profile picture on local storage.
|
* The file name of the contact's profile picture on local storage.
|
||||||
*/
|
*/
|
||||||
var profilePictureFileName: String? = null
|
var profilePictureFileName: String? = null,
|
||||||
/**
|
/**
|
||||||
* The key with which the profile picture is encrypted.
|
* The key with which the profile picture is encrypted.
|
||||||
*/
|
*/
|
||||||
var profilePictureEncryptionKey: ByteArray? = null
|
var profilePictureEncryptionKey: ByteArray? = null,
|
||||||
/**
|
/**
|
||||||
* The ID of the thread associated with this contact.
|
* The ID of the thread associated with this contact.
|
||||||
*/
|
*/
|
||||||
var threadID: Long? = null
|
var threadID: Long? = null,
|
||||||
|
|
||||||
// region Name
|
|
||||||
/**
|
/**
|
||||||
* The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
* The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||||
*/
|
*/
|
||||||
var name: String? = null
|
var name: String? = null,
|
||||||
/**
|
/**
|
||||||
* The contact's nickname, if the user set one.
|
* The contact's nickname, if the user set one.
|
||||||
*/
|
*/
|
||||||
var nickname: String? = null
|
var nickname: String? = null,
|
||||||
|
): Parcelable {
|
||||||
/**
|
/**
|
||||||
* The name to display in the UI. For local use only.
|
* The name to display in the UI. For local use only.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue