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
|
||||
|
||||
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() }
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ plugins {
|
|||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
id 'kotlin-parcelize'
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue