feat(WIP): group creation / navigation / passing and sharing state in vm

This commit is contained in:
0x330a 2023-11-01 16:39:34 +11:00
parent 82a55d256c
commit baf2157331
12 changed files with 339 additions and 89 deletions

View File

@ -47,12 +47,12 @@ android {
useLibrary 'org.apache.http.legacy'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
packagingOptions {
@ -207,6 +207,7 @@ dependencies {
implementation("com.google.dagger:hilt-android:2.46.1")
kapt("com.google.dagger:hilt-android-compiler:2.46")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
implementation "io.github.raamcosta.compose-destinations:core:$composeDestinationsVersion"
ksp "io.github.raamcosta.compose-destinations:ksp:$composeDestinationsVersion"

View File

@ -1,40 +1,42 @@
package org.thoughtcrime.securesms.groups
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
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.utilities.Device
import org.session.libsession.utilities.recipients.Recipient
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.CreateGroupState
import org.thoughtcrime.securesms.groups.compose.CreateGroupNavGraph
import org.thoughtcrime.securesms.groups.compose.SelectContacts
import org.thoughtcrime.securesms.groups.compose.ViewState
import org.thoughtcrime.securesms.groups.destinations.SelectContactScreenDestination
import org.thoughtcrime.securesms.ui.AppTheme
import javax.inject.Inject
@AndroidEntryPoint
class CreateGroupFragment : Fragment() {
@Inject
lateinit var device: Device
private lateinit var binding: FragmentCreateGroupBinding
private val viewModel: CreateGroupViewModel by viewModels()
lateinit var delegate: NewConversationDelegate
@ -43,49 +45,77 @@ class CreateGroupFragment : Fragment() {
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
val getDelegate = { delegate }
setContent {
// this is kind of annoying to require an initial state in the fragment and the VM
val currentState = viewModel.viewState.observeAsState(initial = ViewState.DEFAULT)
// create group state might be useful in future for adding members and returning
// to the create group state with a copy or something
CreateGroupScreen(currentState.value, createGroupState = CreateGroupState("", "", emptySet()))
AppTheme {
DestinationsNavHost(navGraph = NavGraphs.createGroup, dependenciesContainerBuilder = {
dependency(getDelegate)
})
}
}
}
}
private fun openConversationActivity(context: Context, recipient: Recipient) {
val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
context.startActivity(intent)
}
}
@Composable
fun CreateGroupScreen(viewState: ViewState,
createGroupState: CreateGroupState,
modifier: Modifier = Modifier) {
AppTheme {
CreateGroup(
viewState,
createGroupState,
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 ->
openConversationActivity(requireContext(), recipient)
delegate.onDialogClosePressed()
}
}
},
onClose = {
delegate.onDialogClosePressed()
},
onBack = {
delegate.onDialogBackPressed()
@CreateGroupNavGraph(start = true)
@Composable
@Destination
fun CreateGroupScreen(
navigator: DestinationsNavigator,
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()
},
onBack = {
getDelegate().onDialogBackPressed()
}
}
)
}
@CreateGroupNavGraph
@Composable
@Destination
fun SelectContactScreen(
resultNavigator: ResultBackNavigator<Contact?>,
viewModel: CreateGroupViewModel = hiltViewModel(),
getDelegate: () -> NewConversationDelegate
) {
val contacts by viewModel.contacts.observeAsState(initial = emptyList())
val currentMembers by viewModel.createGroupState.observeAsState()
SelectContacts(
contacts - currentMembers?.members.orEmpty(),
onBack = { resultNavigator.navigateBack(null) },
onClose = { getDelegate().onDialogClosePressed() }
)
}

View File

@ -3,9 +3,8 @@ package org.thoughtcrime.securesms.groups
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.liveData
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
@ -23,8 +22,14 @@ class CreateGroupViewModel @Inject constructor(
private val _viewState = MutableLiveData(ViewState.DEFAULT)
val viewState: LiveData<ViewState> = _viewState
val createGroupState: MutableLiveData<CreateGroupState> = MutableLiveData(CreateGroupState("","", emptySet()))
val contacts = liveData {
emit(storage.getAllContacts().toList())
}
init {
viewModelScope.launch {
// viewModelScope.launch {
// threadDb.approvedConversationList.use { openCursor ->
// val reader = threadDb.readerFor(openCursor)
// val recipients = mutableListOf<Recipient>()
@ -36,7 +41,7 @@ class CreateGroupViewModel @Inject constructor(
// .filter { !it.isGroupRecipient && it.hasApprovedMe() && it.address.serialize() != textSecurePreferences.getLocalNumber() }
// }
// }
}
// }
}
fun tryCreateGroup(createGroupState: CreateGroupState): Recipient? {

View File

@ -2,11 +2,14 @@ package org.thoughtcrime.securesms.groups.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
@ -14,7 +17,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
@ -22,49 +25,79 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.home.search.getSearchName
import org.thoughtcrime.securesms.ui.Avatar
import org.thoughtcrime.securesms.ui.LocalPreviewMode
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
@Composable
fun EmptyPlaceholder(modifier: Modifier = Modifier) {
Column {
Column(modifier) {
Text(
text = stringResource(id = R.string.conversation_settings_group_members),
text = stringResource(id = R.string.activity_create_closed_group_empty_placeholer),
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.Start)
.padding(vertical = 8.dp)
)
// TODO group list representation
Text(
text = stringResource(id = R.string.activity_create_closed_group_not_enough_group_members_error),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.fillMaxWidth()
.padding(vertical = 8.dp)
)
}
}
@OptIn(ExperimentalGlideComposeApi::class)
fun LazyListScope.memberList(
contacts: List<Contact>,
modifier: Modifier = Modifier,
onDelete: (Contact) -> Unit
) {
item {
Text(
text = stringResource(id = R.string.conversation_settings_group_members),
style = MaterialTheme.typography.subtitle2,
modifier = modifier
.padding(vertical = 8.dp)
)
}
if (contacts.isEmpty()) {
item {
EmptyPlaceholder(modifier.fillParentMaxWidth())
EmptyPlaceholder(modifier.fillMaxWidth())
}
} else {
items(contacts) { contact ->
Row(modifier) {
ContactPhoto(contact, modifier = Modifier.size(48.dp))
Row(modifier.fillMaxWidth()) {
ContactPhoto(
contact,
modifier = Modifier
.size(48.dp)
.align(CenterVertically)
)
Text(
text = contact.getSearchName(),
fontWeight = FontWeight.Bold,
modifier = Modifier
.weight(1f)
.padding(16.dp)
.align(CenterVertically)
)
Image(
painterResource(id = R.drawable.ic_baseline_close_24),
null,
Modifier
.size(32.dp)
.align(CenterVertically)
.clickable {
onDelete(contact)
}
)
}
}
}
@ -86,9 +119,34 @@ fun RowScope.ContactPhoto(contact: Contact, modifier: Modifier = Modifier) {
)
} else {
val context = LocalContext.current
// Ideally we migrate to something that doesn't require recipient, or get contact photo another way
val recipient = remember(contact) {
Recipient.from(context, Address.fromSerialized(contact.sessionID), false)
}
Avatar(recipient)
}
}
@Preview
@Composable
fun PreviewMemberList(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
val previewMembers = setOf(
Contact(random).apply {
name = "Person"
}
)
PreviewTheme(themeResId = themeResId) {
LazyColumn {
memberList(
previewMembers.toList(),
Modifier.padding(vertical = 8.dp, horizontal = 24.dp)
) { deleted ->
}
}
}
}

View File

@ -1,16 +1,18 @@
package org.thoughtcrime.securesms.groups.compose
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
@ -25,42 +27,44 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
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.ui.CellWithPaddingAndMargin
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.EditableAvatar
import org.thoughtcrime.securesms.ui.NavigationBar
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
data class CreateGroupState (
val groupName: String,
val groupDescription: String,
val members: Set<Contact>
var groupName: String,
var groupDescription: String,
val members: MutableSet<Contact>
)
@Composable
fun CreateGroup(
viewState: ViewState,
createGroupState: CreateGroupState,
createGroupState: MutableLiveData<CreateGroupState>,
onCreate: (CreateGroupState) -> Unit,
onSelectContact: () -> Unit,
onBack: () -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
var name by remember { mutableStateOf(createGroupState.groupName) }
var name by createGroupState
var description by remember { mutableStateOf(createGroupState.groupDescription) }
var members by remember { mutableStateOf(createGroupState.members) }
val scrollState = rememberScrollState()
val lazyState = rememberLazyListState()
val onDeleteMember = { contact: Contact ->
@ -112,6 +116,50 @@ fun CreateGroup(
contentDescription = descriptionDescription
},
)
CellWithPaddingAndMargin(padding = 0.dp) {
Column(Modifier.fillMaxSize()) {
// Select Contacts
val padding = Modifier
.padding(8.dp)
.fillMaxWidth()
Row(padding.clickable {
onSelectContact()
}) {
Image(
painterResource(id = R.drawable.ic_person_white_24dp),
null,
Modifier
.padding(4.dp)
.align(Alignment.CenterVertically)
)
Text(
stringResource(id = R.string.activity_create_closed_group_select_contacts),
Modifier
.padding(4.dp)
.align(Alignment.CenterVertically)
)
}
Divider()
// Add account ID or ONS
Row(padding) {
Image(
painterResource(id = R.drawable.ic_baseline_add_24),
null,
Modifier
.padding(4.dp)
.align(Alignment.CenterVertically)
)
Text(
stringResource(id = R.string.activity_create_closed_group_add_account_or_ons),
Modifier
.padding(4.dp)
.align(Alignment.CenterVertically)
)
}
}
}
}
}
// Group list
@ -155,7 +203,6 @@ fun CreateGroup(
@Preview
@Composable
fun ClosedGroupPreview(
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
) {
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
val previewMembers = setOf(
@ -163,12 +210,13 @@ fun ClosedGroupPreview(
name = "Person"
}
)
PreviewTheme(themeResId) {
PreviewTheme(R.style.Theme_Session_DayNight_NoActionBar_Test) {
CreateGroup(
viewState = ViewState(false, null),
createGroupState = CreateGroupState("Group Name", "Test Group Description", previewMembers),
onCreate = {},
onClose = {},
onSelectContact = {},
onBack = {},
)
}

View File

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.groups.compose
import com.ramcosta.composedestinations.annotation.NavGraph
@NavGraph
annotation class CreateGroupNavGraph(
val start: Boolean = false
)

View File

@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.groups.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
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.res.stringResource
import network.loki.messenger.R
import org.session.libsession.messaging.contacts.Contact
import org.thoughtcrime.securesms.home.search.getSearchName
import org.thoughtcrime.securesms.ui.NavigationBar
import org.thoughtcrime.securesms.ui.SearchBar
@Composable
fun SelectContacts(
contactListState: List<Contact>,
onBack: ()->Unit,
onClose: ()->Unit,
) {
var queryFilter by remember { mutableStateOf("") }
// May introduce more advanced filters
val filtered = if (queryFilter.isEmpty()) contactListState
else {
contactListState
.filter { contact ->
contact.getSearchName()
.contains(queryFilter)
}
}
Column {
NavigationBar(
title = stringResource(id = R.string.activity_create_closed_group_select_contacts),
onBack = onBack,
onClose = onClose
)
LazyColumn {
item {
// Search Bar
SearchBar(queryFilter, onValueChanged = { value -> queryFilter = value })
}
}
}
}

View File

@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.ButtonColors
import androidx.compose.material.Card
import androidx.compose.material.Colors
@ -34,6 +35,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -225,6 +227,41 @@ fun EditableAvatar(
}
}
@Composable
fun SearchBar(
query: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.background(MaterialTheme.colors.primaryVariant, RoundedCornerShape(100))
) {
Image(
painterResource(id = R.drawable.ic_search_24),
contentDescription = null,
colorFilter = ColorFilter.tint(
MaterialTheme.colors.onPrimary
),
modifier = Modifier.size(20.dp)
)
BasicTextField(
singleLine = true,
// label = { Text(text = stringResource(id = R.string.search_contacts_hint),modifier=Modifier.padding(0.dp)) },
value = query,
onValueChange = onValueChanged,
modifier = Modifier
.padding(start = 8.dp)
.padding(4.dp)
.weight(1f),
)
}
}
@Composable
fun NavigationBar(
//
@ -296,3 +333,11 @@ fun PreviewNavigationBar(@PreviewParameter(provider = ThemeResPreviewParameterPr
NavigationBar(title = "Create Group", onBack = {}, onClose = {})
}
}
@Composable
@Preview
fun PreviewSearchBar(@PreviewParameter(provider = ThemeResPreviewParameterProvider::class) themeResId: Int) {
PreviewTheme(themeResId = themeResId) {
SearchBar("", {})
}
}

View File

@ -778,6 +778,7 @@
<string name="activity_create_closed_group_group_name_missing_error">Please enter a group name</string>
<string name="activity_create_closed_group_group_name_too_long_error">Please enter a shorter group name</string>
<string name="activity_create_closed_group_not_enough_group_members_error">Please pick at least 1 group member</string>
<string name="activity_create_closed_group_empty_placeholer">You haven\'t added any members</string>
<string name="activity_create_closed_group_too_many_group_members_error">A closed group cannot have more than 100 members</string>
<string name="activity_join_public_chat_title">Join Open Group</string>
<string name="activity_join_public_chat_error">Couldn\'t join group</string>
@ -1073,4 +1074,6 @@
<string name="dialog_clear_all_messages_clear">Clear</string>
<string name="dialog_clear_all_messages_cancel">Cancel</string>
<string name="media_overview_activity__clear_media">Clear All</string>
<string name="activity_create_closed_group_select_contacts">Select Contacts</string>
<string name="activity_create_closed_group_add_account_or_ons">Add Account ID or ONS</string>
</resources>

View File

@ -30,11 +30,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
}

View File

@ -12,8 +12,8 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}

View File

@ -9,8 +9,8 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}