fix: tests and config sync to use the signingkey variant of the authenticated delete, finish authenticated delete, add and update dependencies for compose and kotlin and add navigation library to make nested composables for create group and future easier to deal with. build preview avatar component
This commit is contained in:
parent
b62cca2612
commit
82a55d256c
|
@ -17,12 +17,12 @@ buildscript {
|
|||
plugins {
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.dagger.hilt.android'
|
||||
id 'com.google.devtools.ksp' version "$kspVersion"
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
|
@ -78,7 +78,7 @@ android {
|
|||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion '1.4.7'
|
||||
kotlinCompilerExtensionVersion '1.5.3'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
@ -206,7 +206,12 @@ android {
|
|||
dependencies {
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.46.1")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.44")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.46")
|
||||
|
||||
implementation "io.github.raamcosta.compose-destinations:core:$composeDestinationsVersion"
|
||||
ksp "io.github.raamcosta.compose-destinations:ksp:$composeDestinationsVersion"
|
||||
|
||||
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
|
@ -330,22 +335,22 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.2"
|
||||
debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.2"
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.3"
|
||||
debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.3"
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||
|
||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
||||
implementation 'androidx.compose.ui:ui:1.5.2'
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.5.2'
|
||||
implementation 'androidx.compose.ui:ui:1.5.3'
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.5.3'
|
||||
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
||||
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
|
||||
implementation "androidx.compose.runtime:runtime-livedata:1.5.2"
|
||||
implementation "androidx.compose.runtime:runtime-livedata:1.5.3"
|
||||
|
||||
implementation 'androidx.compose.foundation:foundation-layout:1.5.2'
|
||||
implementation 'androidx.compose.material:material:1.5.2'
|
||||
implementation 'androidx.compose.foundation:foundation-layout:1.5.3'
|
||||
implementation 'androidx.compose.material:material:1.5.3'
|
||||
}
|
||||
|
||||
static def getLastCommitTimestamp() {
|
||||
|
|
|
@ -13,9 +13,9 @@ import org.junit.Rule
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.groups.CreateGroup
|
||||
import org.thoughtcrime.securesms.groups.CreateGroupFragment
|
||||
import org.thoughtcrime.securesms.groups.CreateGroupState
|
||||
import org.thoughtcrime.securesms.groups.compose.CreateGroup
|
||||
import org.thoughtcrime.securesms.groups.compose.CreateGroupState
|
||||
import org.thoughtcrime.securesms.groups.compose.ViewState
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
@ -39,7 +39,7 @@ class CreateGroupTests {
|
|||
composeTest.setContent {
|
||||
AppTheme {
|
||||
CreateGroup(
|
||||
viewState = CreateGroupFragment.ViewState.DEFAULT,
|
||||
viewState = ViewState.DEFAULT,
|
||||
createGroupState = CreateGroupState("", "", emptySet()),
|
||||
onCreate = { submitted ->
|
||||
postedGroup = submitted
|
||||
|
@ -74,7 +74,7 @@ class CreateGroupTests {
|
|||
composeTest.setContent {
|
||||
AppTheme {
|
||||
CreateGroup(
|
||||
viewState = CreateGroupFragment.ViewState.DEFAULT,
|
||||
viewState = ViewState.DEFAULT,
|
||||
createGroupState = CreateGroupState("", "", emptySet()),
|
||||
onCreate = { submitted ->
|
||||
postedGroup = submitted
|
||||
|
@ -98,22 +98,20 @@ class CreateGroupTests {
|
|||
val application = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
// Accessibility IDs
|
||||
val backDesc = application.getString(R.string.new_conversation_dialog_back_button_content_description)
|
||||
val closeDesc = application.getString(R.string.new_conversation_dialog_close_button_content_description)
|
||||
|
||||
var postedGroup: CreateGroupState? = null
|
||||
var backPressed = false
|
||||
var closePressed = false
|
||||
|
||||
composeTest.setContent {
|
||||
AppTheme {
|
||||
CreateGroup(
|
||||
viewState = CreateGroupFragment.ViewState.DEFAULT,
|
||||
viewState = ViewState.DEFAULT,
|
||||
createGroupState = CreateGroupState("", "", emptySet()),
|
||||
onCreate = { submitted ->
|
||||
postedGroup = submitted
|
||||
},
|
||||
onBack = { backPressed = true },
|
||||
onClose = { closePressed = true })
|
||||
onClose = { })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,22 +127,20 @@ class CreateGroupTests {
|
|||
fun testCloseButton() {
|
||||
val application = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
// Accessibility IDs
|
||||
val backDesc = application.getString(R.string.new_conversation_dialog_back_button_content_description)
|
||||
val closeDesc = application.getString(R.string.new_conversation_dialog_close_button_content_description)
|
||||
|
||||
var postedGroup: CreateGroupState? = null
|
||||
var backPressed = false
|
||||
var closePressed = false
|
||||
|
||||
composeTest.setContent {
|
||||
AppTheme {
|
||||
CreateGroup(
|
||||
viewState = CreateGroupFragment.ViewState.DEFAULT,
|
||||
viewState = ViewState.DEFAULT,
|
||||
createGroupState = CreateGroupState("", "", emptySet()),
|
||||
onCreate = { submitted ->
|
||||
postedGroup = submitted
|
||||
},
|
||||
onBack = { backPressed = true },
|
||||
onBack = { },
|
||||
onClose = { closePressed = true })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,18 +11,16 @@ import org.mockito.Mock
|
|||
import org.mockito.junit.MockitoJUnitRunner
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Hex
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.SessionId
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.ConfigDatabase
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import org.thoughtcrime.securesms.groups.CreateGroupState
|
||||
import org.thoughtcrime.securesms.groups.CreateGroupViewModel
|
||||
import org.thoughtcrime.securesms.groups.compose.CreateGroupState
|
||||
|
||||
@RunWith(MockitoJUnitRunner::class)
|
||||
class ClosedGroupViewTests {
|
||||
|
|
|
@ -6,66 +6,25 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
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.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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
|
||||
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.livedata.observeAsState
|
||||
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
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentCreateGroupBinding
|
||||
import org.session.libsession.avatars.ContactPhoto
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.Device
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
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.ViewState
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
import org.thoughtcrime.securesms.ui.Avatar
|
||||
import org.thoughtcrime.securesms.ui.EditableAvatar
|
||||
import org.thoughtcrime.securesms.ui.LocalPreviewMode
|
||||
import org.thoughtcrime.securesms.ui.NavigationBar
|
||||
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -129,188 +88,4 @@ class CreateGroupFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
data class ViewState(
|
||||
val isLoading: Boolean,
|
||||
@StringRes val error: Int?
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT = ViewState(false, null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class CreateGroupState (
|
||||
val groupName: String,
|
||||
val groupDescription: String,
|
||||
val members: Set<Contact>
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CreateGroup(
|
||||
viewState: CreateGroupFragment.ViewState,
|
||||
createGroupState: CreateGroupState,
|
||||
onCreate: (CreateGroupState) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier) {
|
||||
|
||||
var name by remember { mutableStateOf(createGroupState.groupName) }
|
||||
var description by remember { mutableStateOf(createGroupState.groupDescription) }
|
||||
var members by remember { mutableStateOf(createGroupState.members) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val lazyState = rememberLazyListState()
|
||||
|
||||
val onDeleteMember = { contact: Contact ->
|
||||
members -= contact
|
||||
}
|
||||
|
||||
Box {
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxWidth()) {
|
||||
LazyColumn(state = lazyState) {
|
||||
// Top bar
|
||||
item {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
NavigationBar(
|
||||
title = stringResource(id = R.string.activity_create_group_title),
|
||||
onBack = onBack,
|
||||
onClose = onClose
|
||||
)
|
||||
// Editable avatar (future chunk)
|
||||
EditableAvatar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 16.dp)
|
||||
)
|
||||
// Title
|
||||
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||
.semantics {
|
||||
contentDescription = nameDescription
|
||||
},
|
||||
)
|
||||
// Description
|
||||
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||
.semantics {
|
||||
contentDescription = descriptionDescription
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
// Group list
|
||||
memberList(contacts = members.toList(), modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp), onDeleteMember)
|
||||
}
|
||||
// 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,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(16.dp)
|
||||
.semantics {
|
||||
contentDescription = createDescription
|
||||
}
|
||||
,
|
||||
shape = RoundedCornerShape(32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.activity_create_group_create_button_title),
|
||||
// TODO: colours of everything here probably needs to be redone
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.width(160.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
if (viewState.isLoading) {
|
||||
Box(modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Gray.copy(alpha = 0.5f))) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun LazyListScope.memberList(contacts: List<Contact>, modifier: Modifier = Modifier, onDelete: (Contact)->Unit) {
|
||||
if (contacts.isEmpty()) {
|
||||
item {
|
||||
EmptyPlaceholder(modifier.fillParentMaxWidth())
|
||||
}
|
||||
} else {
|
||||
items(contacts) { contact ->
|
||||
Row(modifier) {
|
||||
val context = LocalContext.current
|
||||
Avatar(Recipient.from(context, Address.fromSerialized(contact.sessionID), false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyPlaceholder(modifier: Modifier = Modifier) {
|
||||
Column {
|
||||
Text(text = stringResource(id = R.string.conversation_settings_group_members),
|
||||
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)
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ClosedGroupPreview(
|
||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||
) {
|
||||
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
|
||||
val previewMembers = setOf(
|
||||
Contact(random).apply {
|
||||
name = "Person"
|
||||
}
|
||||
)
|
||||
PreviewTheme(themeResId) {
|
||||
CreateGroup(
|
||||
viewState = CreateGroupFragment.ViewState(false, null),
|
||||
createGroupState = CreateGroupState("Group Name", "Test Group Description", previewMembers),
|
||||
onCreate = {},
|
||||
onClose = {},
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Contact.contactPhoto(): ContactPhoto {
|
||||
if (LocalPreviewMode.current) {
|
||||
//
|
||||
}
|
||||
TODO()
|
||||
}
|
|
@ -10,6 +10,8 @@ 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
|
||||
|
@ -18,11 +20,8 @@ class CreateGroupViewModel @Inject constructor(
|
|||
private val storage: Storage,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _recipients = MutableLiveData<List<Recipient>>()
|
||||
val recipients: LiveData<List<Recipient>> = _recipients
|
||||
|
||||
private val _viewState = MutableLiveData(CreateGroupFragment.ViewState.DEFAULT)
|
||||
val viewState: LiveData<CreateGroupFragment.ViewState> = _viewState
|
||||
private val _viewState = MutableLiveData(ViewState.DEFAULT)
|
||||
val viewState: LiveData<ViewState> = _viewState
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
@ -41,7 +40,7 @@ class CreateGroupViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun tryCreateGroup(createGroupState: CreateGroupState): Recipient? {
|
||||
_viewState.postValue(CreateGroupFragment.ViewState(true, null))
|
||||
_viewState.postValue(ViewState(true, null))
|
||||
|
||||
val name = createGroupState.groupName
|
||||
val description = createGroupState.groupDescription
|
||||
|
@ -51,7 +50,7 @@ class CreateGroupViewModel @Inject constructor(
|
|||
// need a name
|
||||
if (name.isEmpty()) {
|
||||
_viewState.postValue(
|
||||
CreateGroupFragment.ViewState(false, R.string.error)
|
||||
ViewState(false, R.string.error)
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
@ -62,21 +61,15 @@ class CreateGroupViewModel @Inject constructor(
|
|||
|
||||
if (members.size <= 1) {
|
||||
_viewState.postValue(
|
||||
CreateGroupFragment.ViewState(false, R.string.activity_create_closed_group_not_enough_group_members_error)
|
||||
ViewState(false, 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(CreateGroupFragment.ViewState(isLoading = false, null))
|
||||
_viewState.postValue(ViewState(isLoading = false, null))
|
||||
}
|
||||
return newGroup.orNull()
|
||||
}
|
||||
|
||||
fun filter(query: String): List<Recipient> {
|
||||
return _recipients.value?.filter {
|
||||
it.address.serialize().contains(query, ignoreCase = true) || it.name?.contains(query, ignoreCase = true) == true
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package org.thoughtcrime.securesms.groups.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
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.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.ui.Avatar
|
||||
import org.thoughtcrime.securesms.ui.LocalPreviewMode
|
||||
|
||||
|
||||
@Composable
|
||||
fun EmptyPlaceholder(modifier: Modifier = Modifier) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.conversation_settings_group_members),
|
||||
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)
|
||||
.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalGlideComposeApi::class)
|
||||
fun LazyListScope.memberList(
|
||||
contacts: List<Contact>,
|
||||
modifier: Modifier = Modifier,
|
||||
onDelete: (Contact) -> Unit
|
||||
) {
|
||||
if (contacts.isEmpty()) {
|
||||
item {
|
||||
EmptyPlaceholder(modifier.fillParentMaxWidth())
|
||||
}
|
||||
} else {
|
||||
items(contacts) { contact ->
|
||||
Row(modifier) {
|
||||
ContactPhoto(contact, modifier = Modifier.size(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun RowScope.ContactPhoto(contact: Contact, modifier: Modifier = Modifier) {
|
||||
return if (LocalPreviewMode.current) {
|
||||
Image(
|
||||
painterResource(id = R.drawable.ic_profile_default),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colors.onPrimary),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = null,
|
||||
modifier = modifier
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.border(1.dp, MaterialTheme.colors.onPrimary, CircleShape)
|
||||
)
|
||||
} else {
|
||||
val context = LocalContext.current
|
||||
val recipient = remember(contact) {
|
||||
Recipient.from(context, Address.fromSerialized(contact.sessionID), false)
|
||||
}
|
||||
Avatar(recipient)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
package org.thoughtcrime.securesms.groups.compose
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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
|
||||
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
|
||||
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 network.loki.messenger.R
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
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>
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun CreateGroup(
|
||||
viewState: ViewState,
|
||||
createGroupState: CreateGroupState,
|
||||
onCreate: (CreateGroupState) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
var name by remember { mutableStateOf(createGroupState.groupName) }
|
||||
var description by remember { mutableStateOf(createGroupState.groupDescription) }
|
||||
var members by remember { mutableStateOf(createGroupState.members) }
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val lazyState = rememberLazyListState()
|
||||
|
||||
val onDeleteMember = { contact: Contact ->
|
||||
members -= contact
|
||||
}
|
||||
|
||||
Box {
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxWidth()) {
|
||||
LazyColumn(state = lazyState) {
|
||||
// Top bar
|
||||
item {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
NavigationBar(
|
||||
title = stringResource(id = R.string.activity_create_group_title),
|
||||
onBack = onBack,
|
||||
onClose = onClose
|
||||
)
|
||||
// Editable avatar (future chunk)
|
||||
EditableAvatar(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 16.dp)
|
||||
)
|
||||
// Title
|
||||
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||
.semantics {
|
||||
contentDescription = nameDescription
|
||||
},
|
||||
)
|
||||
// Description
|
||||
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||
.semantics {
|
||||
contentDescription = descriptionDescription
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
// Group list
|
||||
memberList(contacts = members.toList(), modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp), onDeleteMember)
|
||||
}
|
||||
// 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,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(16.dp)
|
||||
.semantics {
|
||||
contentDescription = createDescription
|
||||
}
|
||||
,
|
||||
shape = RoundedCornerShape(32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.activity_create_group_create_button_title),
|
||||
// TODO: colours of everything here probably needs to be redone
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.width(160.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
if (viewState.isLoading) {
|
||||
Box(modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Gray.copy(alpha = 0.5f))) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ClosedGroupPreview(
|
||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||
) {
|
||||
val random = "05abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
|
||||
val previewMembers = setOf(
|
||||
Contact(random).apply {
|
||||
name = "Person"
|
||||
}
|
||||
)
|
||||
PreviewTheme(themeResId) {
|
||||
CreateGroup(
|
||||
viewState = ViewState(false, null),
|
||||
createGroupState = CreateGroupState("Group Name", "Test Group Description", previewMembers),
|
||||
onCreate = {},
|
||||
onClose = {},
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ViewState(
|
||||
val isLoading: Boolean,
|
||||
@StringRes val error: Int?
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT = ViewState(false, null)
|
||||
}
|
||||
}
|
|
@ -18,7 +18,9 @@ org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
|||
org.gradle.unsafe.configuration-cache=true
|
||||
|
||||
googleServicesVersion=4.3.12
|
||||
kotlinVersion=1.8.21
|
||||
kotlinVersion=1.9.10
|
||||
kspVersion=1.9.10-1.0.13
|
||||
composeDestinationsVersion=1.9.54
|
||||
android.useAndroidX=true
|
||||
appcompatVersion=1.6.1
|
||||
coreVersion=1.8.0
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.session.libsession.messaging.utilities.Data
|
|||
import org.session.libsession.snode.RawResponse
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.snode.SnodeAPI.SnodeBatchRequestInfo
|
||||
import org.session.libsession.snode.SnodeAPI.signingKeyCallback
|
||||
import org.session.libsession.snode.SnodeMessage
|
||||
import org.session.libsession.utilities.ConfigFactoryProtocol
|
||||
import org.session.libsignal.utilities.Base64
|
||||
|
@ -179,11 +180,28 @@ data class ConfigurationSyncJob(val destination: Destination) : Job {
|
|||
val toDeleteRequest =
|
||||
toDeleteHashes.let { toDeleteFromAllNamespaces ->
|
||||
if (toDeleteFromAllNamespaces.isEmpty()) null
|
||||
else
|
||||
SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||
destination.destinationPublicKey(),
|
||||
toDeleteFromAllNamespaces
|
||||
)
|
||||
else if (destination is Destination.ClosedGroup) {
|
||||
// Build sign callback for group's admin key
|
||||
val signingKey =
|
||||
configFactory.userGroups
|
||||
?.getClosedGroup(destination.publicKey)
|
||||
?.adminKey ?: return@let null
|
||||
|
||||
val signCallback = signingKeyCallback(signingKey)
|
||||
|
||||
// Destination is a closed group swarm, build with signCallback
|
||||
SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||
destination.destinationPublicKey(),
|
||||
toDeleteFromAllNamespaces,
|
||||
signCallback
|
||||
)
|
||||
} else {
|
||||
// Destination is our own swarm
|
||||
SnodeAPI.buildAuthenticatedDeleteBatchInfo(
|
||||
destination.destinationPublicKey(),
|
||||
toDeleteFromAllNamespaces
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val allRequests = mutableListOf<SnodeBatchRequestInfo>()
|
||||
|
|
|
@ -428,22 +428,19 @@ object SnodeAPI {
|
|||
* @param required indicates that *at least one* message in the list is deleted from the server, otherwise it will return 404
|
||||
*/
|
||||
fun buildAuthenticatedDeleteBatchInfo(publicKey: String, messageHashes: List<String>, required: Boolean = false): SnodeBatchRequestInfo? {
|
||||
val params = mutableMapOf(
|
||||
"pubkey" to publicKey,
|
||||
"required" to required, // could be omitted technically but explicit here
|
||||
"messages" to messageHashes
|
||||
)
|
||||
val userEd25519KeyPair = try {
|
||||
MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
|
||||
val ed25519PublicKey = userEd25519KeyPair.publicKey
|
||||
val signCallback = signingKeyCallback(userEd25519KeyPair.secretKey.asBytes)
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.DeleteMessage.rawValue,
|
||||
params,
|
||||
null
|
||||
return buildAuthenticatedDeleteBatchInfo(
|
||||
publicKey,
|
||||
messageHashes,
|
||||
signCallback,
|
||||
required,
|
||||
ed25519PublicKey
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -451,8 +448,26 @@ object SnodeAPI {
|
|||
publicKey: String,
|
||||
messageHashes: List<String>,
|
||||
signCallback: SignCallback,
|
||||
required: Boolean = false): SnodeBatchRequestInfo? {
|
||||
val verificationData = "delete${messageHashes.joinToString("")}".toByteArray()
|
||||
required: Boolean = false,
|
||||
ed25519PubKey: Key? = null): SnodeBatchRequestInfo {
|
||||
val verificationData = "delete${messageHashes.joinToString("")}"
|
||||
val params = mutableMapOf(
|
||||
"pubkey" to publicKey,
|
||||
"required" to required, // could be omitted technically but explicit here
|
||||
"messages" to messageHashes
|
||||
)
|
||||
|
||||
if (ed25519PubKey != null) {
|
||||
params += "pubkey_ed25519" to ed25519PubKey.asHexString
|
||||
}
|
||||
|
||||
params += signCallback(verificationData, null, null)
|
||||
|
||||
return SnodeBatchRequestInfo(
|
||||
Snode.Method.DeleteMessage.rawValue,
|
||||
params,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
fun buildAuthenticatedRetrieveBatchRequest(snode: Snode,
|
||||
|
@ -681,10 +696,13 @@ object SnodeAPI {
|
|||
throw Error.SigningFailed
|
||||
}
|
||||
val params = mutableMapOf<String,Any>(
|
||||
"timestamp" to timestamp,
|
||||
"signature" to Base64.encodeBytes(signature),
|
||||
"signature" to Base64.encodeBytes(signature),
|
||||
)
|
||||
if (namespace != Namespace.DEFAULT()) {
|
||||
if (timestamp != null) {
|
||||
params += "timestamp" to timestamp
|
||||
}
|
||||
|
||||
if (namespace != null && namespace != Namespace.DEFAULT()) {
|
||||
params += "namespace" to namespace
|
||||
}
|
||||
params
|
||||
|
@ -692,13 +710,15 @@ object SnodeAPI {
|
|||
|
||||
fun subkeyCallback(authData: ByteArray, groupKeysConfig: GroupKeysConfig, freeAfter: Boolean = true): SignCallback = { message, timestamp, namespace ->
|
||||
val (subaccount, subaccountSig, sig) = groupKeysConfig.subAccountSign(message.toByteArray(),authData)
|
||||
val params = mutableMapOf(
|
||||
val params = mutableMapOf<String, Any>(
|
||||
"subaccount" to subaccount,
|
||||
"subaccount_sig" to subaccountSig,
|
||||
"signature" to sig,
|
||||
"timestamp" to timestamp,
|
||||
)
|
||||
if (namespace != Namespace.DEFAULT()) {
|
||||
if (timestamp != null) {
|
||||
params += "timestamp" to timestamp
|
||||
}
|
||||
if (namespace != null && namespace != Namespace.DEFAULT()) {
|
||||
params += "namespace" to namespace
|
||||
}
|
||||
if (freeAfter) {
|
||||
|
@ -1022,7 +1042,7 @@ object SnodeAPI {
|
|||
}
|
||||
}
|
||||
|
||||
typealias SignCallback = (String, Long, Int)->Map<String,Any>
|
||||
typealias SignCallback = (String, Long?, Int?)->Map<String,Any>
|
||||
|
||||
// Type Aliases
|
||||
typealias RawResponse = Map<*, *>
|
||||
|
|
Loading…
Reference in New Issue