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
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
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.ResultRecipient
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.databinding.FragmentCreateGroupBinding
import org.session.libsession.messaging.contacts.Contact
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.CreateGroupNavGraph
import org.thoughtcrime.securesms.groups.compose.SelectContacts
@ -63,38 +59,20 @@ class CreateGroupFragment : Fragment() {
@Destination
fun CreateGroupScreen(
navigator: DestinationsNavigator,
resultSelectContact: ResultRecipient<SelectContactScreenDestination, Contact?>,
resultSelectContact: ResultRecipient<SelectContactScreenDestination, Contact>,
viewModel: CreateGroupViewModel = hiltViewModel(),
getDelegate: () -> NewConversationDelegate
) {
val viewState by viewModel.viewState.observeAsState(ViewState.DEFAULT)
val lifecycleScope = rememberCoroutineScope()
val context = LocalContext.current
val currentGroupState = viewModel.createGroupState
CreateGroup(
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 = {
getDelegate().onDialogClosePressed()
},
onSelectContact = { navigator.navigate(SelectContactScreenDestination) },
onBack = {
getDelegate().onDialogBackPressed()
}
@ -105,17 +83,18 @@ fun CreateGroupScreen(
@Composable
@Destination
fun SelectContactScreen(
resultNavigator: ResultBackNavigator<Contact?>,
resultNavigator: ResultBackNavigator<Contact>,
viewModel: CreateGroupViewModel = hiltViewModel(),
getDelegate: () -> NewConversationDelegate
) {
val viewState by viewModel.viewState.observeAsState(ViewState.DEFAULT)
val currentMembers = viewState.members
val contacts by viewModel.contacts.observeAsState(initial = emptyList())
val currentMembers by viewModel.createGroupState.observeAsState()
SelectContacts(
contacts - currentMembers?.members.orEmpty(),
onBack = { resultNavigator.navigateBack(null) },
contacts - currentMembers,
onBack = { resultNavigator.navigateBack() },
onClose = { getDelegate().onDialogClosePressed() }
)
}

View File

@ -6,56 +6,36 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import dagger.hilt.android.lifecycle.HiltViewModel
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.groups.compose.CreateGroupState
import org.thoughtcrime.securesms.groups.compose.ViewState
import javax.inject.Inject
@HiltViewModel
class CreateGroupViewModel @Inject constructor(
private val textSecurePreferences: TextSecurePreferences,
private val storage: Storage,
) : ViewModel() {
private val _viewState = MutableLiveData(ViewState.DEFAULT)
val viewState: LiveData<ViewState> = _viewState
val createGroupState: MutableLiveData<CreateGroupState> = MutableLiveData(CreateGroupState("","", emptySet()))
val contacts = liveData { emit(storage.getAllContacts()) }
val contacts = liveData {
emit(storage.getAllContacts().toList())
}
fun tryCreateGroup(): Recipient? {
init {
// 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() }
// }
// }
// }
}
val currentState = _viewState.value!!
fun tryCreateGroup(createGroupState: CreateGroupState): Recipient? {
_viewState.postValue(ViewState(true, null))
_viewState.postValue(currentState.copy(isLoading = true, error = null))
val name = createGroupState.groupName
val description = createGroupState.groupDescription
val members = createGroupState.members.toMutableSet()
val name = currentState.name
val description = currentState.description
val members = currentState.members.toMutableSet()
// do some validation
// need a name
if (name.isEmpty()) {
_viewState.postValue(
ViewState(false, R.string.error)
currentState.copy(isLoading = false, error = R.string.error)
)
return null
}
@ -66,14 +46,14 @@ class CreateGroupViewModel @Inject constructor(
if (members.size <= 1) {
_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
val newGroup = storage.createNewGroup(name, description, members)
if (!newGroup.isPresent) {
_viewState.postValue(ViewState(isLoading = false, null))
_viewState.postValue(currentState.copy(isLoading = false, error = null))
}
return newGroup.orNull()
}

View File

@ -20,10 +20,6 @@ import androidx.compose.material.OutlinedButton
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
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.Modifier
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import network.loki.messenger.R
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.Divider
import org.thoughtcrime.securesms.ui.EditableAvatar
@ -53,24 +49,14 @@ data class CreateGroupState (
@Composable
fun CreateGroup(
viewState: ViewState,
createGroupState: MutableLiveData<CreateGroupState>,
onCreate: (CreateGroupState) -> Unit,
onSelectContact: () -> Unit,
onBack: () -> Unit,
onClose: () -> Unit,
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 onDeleteMember = { contact: Contact ->
members -= contact
}
Box {
Column(
modifier
@ -93,8 +79,8 @@ fun CreateGroup(
// Title
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
OutlinedTextField(
value = name,
onValueChange = { name = it },
value = viewState.name,
onValueChange = { viewState.updateState(StateUpdate.Name(it)) },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
@ -106,8 +92,8 @@ fun CreateGroup(
// Description
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
OutlinedTextField(
value = description,
onValueChange = { description = it },
value = viewState.description,
onValueChange = { viewState.updateState(StateUpdate.Description(it)) },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
@ -163,13 +149,15 @@ fun CreateGroup(
}
}
// 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
val createDescription = stringResource(id = R.string.AccessibilityId_create_closed_group_create_button)
OutlinedButton(
onClick = { onCreate(CreateGroupState(name, description, members)) },
enabled = name.isNotBlank() && !viewState.isLoading,
onClick = { viewState.create() },
enabled = viewState.canCreate,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
@ -212,21 +200,38 @@ fun ClosedGroupPreview(
)
PreviewTheme(R.style.Theme_Session_DayNight_NoActionBar_Test) {
CreateGroup(
viewState = ViewState(false, null),
createGroupState = CreateGroupState("Group Name", "Test Group Description", previewMembers),
onCreate = {},
onClose = {},
viewState = ViewState.DEFAULT.copy(
// override any preview parameters
),
onSelectContact = {},
onBack = {},
onClose = {},
)
}
}
data class ViewState(
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 {
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 'kotlin-android'
id 'kotlinx-serialization'
id 'kotlin-parcelize'
}
android {

View File

@ -1,34 +1,37 @@
package org.session.libsession.messaging.contacts
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
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.
*/
var profilePictureURL: String? = null
var profilePictureURL: String? = null,
/**
* 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.
*/
var profilePictureEncryptionKey: ByteArray? = null
var profilePictureEncryptionKey: ByteArray? = null,
/**
* The ID of the thread associated with this contact.
*/
var threadID: Long? = null
// region Name
var threadID: Long? = null,
/**
* 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.
*/
var nickname: String? = null
var nickname: String? = null,
): Parcelable {
/**
* The name to display in the UI. For local use only.
*/