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:
0x330a 2023-10-30 17:45:15 +11:00
parent b62cca2612
commit 82a55d256c
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
10 changed files with 379 additions and 294 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<*, *>