fix: view model should give and receive better structured events and state

This commit is contained in:
0x330a 2023-11-02 12:27:23 +11:00
parent baf2157331
commit fabe2d44da
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
5 changed files with 64 additions and 96 deletions

View File

@ -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() }
) )
} }

View File

@ -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()
} }

View File

@ -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()
}
} }

View File

@ -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 {

View File

@ -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.
*/ */