feat: add background group creation and placeholder spinny progress

This commit is contained in:
0x330a
2023-09-18 17:26:34 +10:00
parent ec02087c6b
commit fc57d3396d
6 changed files with 171 additions and 73 deletions

View File

@@ -7,15 +7,19 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton import androidx.compose.material.OutlinedButton
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
@@ -28,6 +32,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -36,7 +41,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentCreateGroupBinding import network.loki.messenger.databinding.FragmentCreateGroupBinding
import org.session.libsession.utilities.Device import org.session.libsession.utilities.Device
@@ -93,9 +101,13 @@ class CreateGroupFragment : Fragment() {
createGroupState, createGroupState,
onCreate = { newGroup -> onCreate = { newGroup ->
// launch something to create here // launch something to create here
val groupRecipient = viewModel.tryCreateGroup(newGroup) // dunno if we want to key this here as a launched effect on some property :thinking:
groupRecipient?.let { recipient -> lifecycleScope.launch(Dispatchers.IO) {
openConversationActivity(requireContext(), recipient) val groupRecipient = viewModel.tryCreateGroup(newGroup)
groupRecipient?.let { recipient ->
openConversationActivity(requireContext(), recipient)
delegate.onDialogClosePressed()
}
} }
}, },
onClose = { onClose = {
@@ -110,11 +122,10 @@ class CreateGroupFragment : Fragment() {
data class ViewState( data class ViewState(
val isLoading: Boolean, val isLoading: Boolean,
@StringRes val error: Int?, @StringRes val error: Int?
val createdThreadId: Long?
) { ) {
companion object { companion object {
val DEFAULT = ViewState(false, null, null) val DEFAULT = ViewState(false, null)
} }
} }
@@ -141,59 +152,68 @@ fun CreateGroup(
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Box {
modifier Column(
.fillMaxWidth()) { modifier
Column(modifier.scrollable(scrollState, orientation = Orientation.Vertical)) { .fillMaxWidth()) {
// Top bar Column(modifier.scrollable(scrollState, orientation = Orientation.Vertical)) {
NavigationBar( // Top bar
title = stringResource(id = R.string.activity_create_group_title), NavigationBar(
onBack = onBack, title = stringResource(id = R.string.activity_create_group_title),
onClose = onClose onBack = onBack,
) onClose = onClose
// Editable avatar (future chunk) )
EditableAvatar( // Editable avatar (future chunk)
EditableAvatar(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 16.dp)
)
// Title
OutlinedTextField(
value = name,
onValueChange = { name = it },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp, horizontal = 24.dp),
)
// Description
OutlinedTextField(
value = description,
onValueChange = { description = it },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp, horizontal = 24.dp),
)
// Group list
MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp))
}
// Create button
OutlinedButton(
onClick = { onCreate(CreateGroupState(name, description, members)) },
enabled = name.isNotBlank() && !viewState.isLoading,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
.padding(top = 16.dp) .padding(16.dp),
) shape = RoundedCornerShape(32.dp)
// Title ) {
OutlinedTextField( Text(
value = name, text = stringResource(id = R.string.activity_create_group_create_button_title),
onValueChange = { name = it }, // TODO: colours of everything here probably needs to be redone
modifier = Modifier color = MaterialTheme.colors.onBackground,
.fillMaxWidth() modifier = Modifier.width(160.dp),
.align(Alignment.CenterHorizontally) textAlign = TextAlign.Center
.padding(vertical = 8.dp, horizontal = 24.dp), )
) }
// Description
OutlinedTextField(
value = description,
onValueChange = { description = it },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp, horizontal = 24.dp),
)
// Group list
MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp))
} }
// Create button if (viewState.isLoading) {
OutlinedButton( Box(modifier = modifier.fillMaxSize().background(Color.Gray.copy(alpha = 0.5f))) {
onClick = { onCreate(CreateGroupState(name, description, members)) }, CircularProgressIndicator(
enabled = name.isNotBlank() && !viewState.isLoading, modifier = Modifier.align(Alignment.Center)
modifier = Modifier )
.align(Alignment.CenterHorizontally) }
.padding(16.dp),
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
)
} }
} }
} }
@@ -226,7 +246,7 @@ fun ClosedGroupPreview(
) { ) {
PreviewTheme(themeResId) { PreviewTheme(themeResId) {
CreateGroup( CreateGroup(
viewState = CreateGroupFragment.ViewState(false, null, null), viewState = CreateGroupFragment.ViewState(false, null),
createGroupState = CreateGroupState("Group Name", "Test Group Description", emptySet()), createGroupState = CreateGroupState("Group Name", "Test Group Description", emptySet()),
onCreate = {}, onCreate = {},
onClose = {}, onClose = {},

View File

@@ -41,23 +41,26 @@ class CreateGroupViewModel @Inject constructor(
} }
fun tryCreateGroup(createGroupState: CreateGroupState): Recipient? { fun tryCreateGroup(createGroupState: CreateGroupState): Recipient? {
_viewState.postValue(CreateGroupFragment.ViewState(true, null, null)) _viewState.postValue(CreateGroupFragment.ViewState(true, null))
val name = createGroupState.groupName val name = createGroupState.groupName
val description = createGroupState.groupDescription val description = createGroupState.groupDescription
val members = createGroupState.members val members = createGroupState.members
// do some validations // do some validation
if (name.isEmpty()) { if (name.isEmpty()) {
_viewState.postValue( _viewState.postValue(
CreateGroupFragment.ViewState(false, R.string.error, null) CreateGroupFragment.ViewState(false, R.string.error)
) )
return null return null
} }
// TODO: add future validation for empty group ? we'll add ourselves anyway ig // TODO: add future validation for empty group ? we'll add ourselves anyway ig
// make a group // make a group
val newGroup = storage.createNewGroup(name, description, members) // TODO: handle optional val newGroup = storage.createNewGroup(name, description, members) // TODO: handle optional
if (!newGroup.isPresent) {
_viewState.postValue(CreateGroupFragment.ViewState(isLoading = false, null))
}
return newGroup.orNull() return newGroup.orNull()
} }

View File

@@ -198,3 +198,20 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_keys(JNIEnv *env, j
} }
return our_stack; return our_stack;
} }
extern "C"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_GroupKeysConfig_currentHashes(JNIEnv *env,
jobject thiz) {
auto ptr = ptrToKeys(env, thiz);
auto existing = ptr->current_hashes();
jclass stack = env->FindClass("java/util/Stack");
jmethodID init = env->GetMethodID(stack, "<init>", "()V");
jobject our_list = env->NewObject(stack, init);
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
for (auto& hash : existing) {
auto hash_bytes = env->NewStringUTF(hash.data());
env->CallObjectMethod(our_list, push, hash_bytes);
}
return our_list;
}

View File

@@ -292,7 +292,8 @@ class GroupMembersConfig(pointer: Long): ConfigBase(pointer), Closeable {
} }
} }
sealed class ConfigSig(pointer: Long) : Config(pointer) sealed class ConfigSig(pointer: Long) : Config(pointer) {
}
class GroupKeysConfig(pointer: Long): ConfigSig(pointer) { class GroupKeysConfig(pointer: Long): ConfigSig(pointer) {
companion object { companion object {
@@ -322,6 +323,7 @@ class GroupKeysConfig(pointer: Long): ConfigSig(pointer) {
external fun needsRekey(): Boolean external fun needsRekey(): Boolean
external fun pendingKey(): ByteArray? external fun pendingKey(): ByteArray?
external fun pendingConfig(): ByteArray? external fun pendingConfig(): ByteArray?
external fun currentHashes(): List<String>
external fun rekey(info: GroupInfoConfig, members: GroupMembersConfig): ByteArray external fun rekey(info: GroupInfoConfig, members: GroupMembersConfig): ByteArray
override fun close() { override fun close() {
free() free()

View File

@@ -23,6 +23,7 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.SessionId import org.session.libsignal.utilities.SessionId
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
import kotlin.time.Duration.Companion.days
class ClosedGroupPoller(private val executor: CoroutineScope, class ClosedGroupPoller(private val executor: CoroutineScope,
private val closedGroupSessionId: SessionId, private val closedGroupSessionId: SessionId,
@@ -97,6 +98,12 @@ class ClosedGroupPoller(private val executor: CoroutineScope,
val members = configFactoryProtocol.getGroupMemberConfig(closedGroupSessionId) ?: return null val members = configFactoryProtocol.getGroupMemberConfig(closedGroupSessionId) ?: return null
val keys = configFactoryProtocol.getGroupKeysConfig(closedGroupSessionId) ?: return null val keys = configFactoryProtocol.getGroupKeysConfig(closedGroupSessionId) ?: return null
val hashesToExtend = mutableSetOf<String>()
hashesToExtend += info.currentHashes()
hashesToExtend += members.currentHashes()
hashesToExtend += keys.currentHashes()
val keysIndex = 0 val keysIndex = 0
val infoIndex = 1 val infoIndex = 1
val membersIndex = 2 val membersIndex = 2
@@ -133,14 +140,26 @@ class ClosedGroupPoller(private val executor: CoroutineScope,
group.signingKey() group.signingKey()
) ?: return null ) ?: return null
val requests = mutableListOf(keysPoll, infoPoll, membersPoll, messagePoll)
if (hashesToExtend.isNotEmpty()) {
SnodeAPI.buildAuthenticatedAlterTtlBatchRequest(
messageHashes = hashesToExtend.toList(),
publicKey = closedGroupSessionId.hexString(),
signingKey = group.signingKey(),
newExpiry = SnodeAPI.nowWithOffset + 14.days.inWholeMilliseconds,
extend = true
)?.let { extensionRequest ->
requests += extensionRequest
}
}
val pollResult = SnodeAPI.getRawBatchResponse( val pollResult = SnodeAPI.getRawBatchResponse(
snode, snode,
closedGroupSessionId.hexString(), closedGroupSessionId.hexString(),
listOf(keysPoll, infoPoll, membersPoll, messagePoll) requests
).get() ).get()
// TODO: add the extend duration TTLs for known hashes here
// if poll result body is null here we don't have any things ig // if poll result body is null here we don't have any things ig
if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Poll results @${SnodeAPI.nowWithOffset}:") if (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Poll results @${SnodeAPI.nowWithOffset}:")
(pollResult["results"] as List<RawResponse>).forEachIndexed { index, response -> (pollResult["results"] as List<RawResponse>).forEachIndexed { index, response ->

View File

@@ -525,9 +525,11 @@ object SnodeAPI {
messageHashes: List<String>, messageHashes: List<String>,
newExpiry: Long, newExpiry: Long,
publicKey: String, publicKey: String,
signingKey: ByteArray,
pubKeyEd25519: String? = null,
shorten: Boolean = false, shorten: Boolean = false,
extend: Boolean = false): SnodeBatchRequestInfo? { extend: Boolean = false): SnodeBatchRequestInfo? {
val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten) ?: return null val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, signingKey, pubKeyEd25519, extend, shorten) ?: return null
return SnodeBatchRequestInfo( return SnodeBatchRequestInfo(
Snode.Method.Expire.rawValue, Snode.Method.Expire.rawValue,
params, params,
@@ -535,6 +537,28 @@ object SnodeAPI {
) )
} }
fun buildAuthenticatedAlterTtlBatchRequest(
messageHashes: List<String>,
newExpiry: Long,
publicKey: String,
shorten: Boolean = false,
extend: Boolean = false): SnodeBatchRequestInfo? {
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
val signingKey = userEd25519KeyPair.secretKey.asBytes
val pubKeyEd25519 = userEd25519KeyPair.publicKey.asHexString
return buildAuthenticatedAlterTtlBatchRequest(
messageHashes,
newExpiry,
publicKey,
signingKey,
pubKeyEd25519,
shorten,
extend
)
}
fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>, sequence: Boolean = false): RawResponsePromise { fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List<SnodeBatchRequestInfo>, sequence: Boolean = false): RawResponsePromise {
val parameters = mutableMapOf<String, Any>( val parameters = mutableMapOf<String, Any>(
"requests" to requests "requests" to requests
@@ -587,9 +611,18 @@ object SnodeAPI {
} }
} }
fun alterTtl(messageHashes: List<String>, newExpiry: Long, publicKey: String, extend: Boolean = false, shorten: Boolean = false): RawResponsePromise { fun alterTtl(messageHashes: List<String>,
newExpiry: Long,
publicKey: String,
extend: Boolean = false,
shorten: Boolean = false): RawResponsePromise {
return retryIfNeeded(maxRetryCount) { return retryIfNeeded(maxRetryCount) {
val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten) val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(
Exception("No user key pair to sign alter ttl message")
)
val signingKey = userEd25519KeyPair.secretKey.asBytes
val pubKeyEd25519 = userEd25519KeyPair.publicKey.asHexString
val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, signingKey, pubKeyEd25519, extend, shorten)
?: return@retryIfNeeded Promise.ofFail( ?: return@retryIfNeeded Promise.ofFail(
Exception("Couldn't build signed params for alterTtl request for newExpiry=$newExpiry, extend=$extend, shorten=$shorten") Exception("Couldn't build signed params for alterTtl request for newExpiry=$newExpiry, extend=$extend, shorten=$shorten")
) )
@@ -599,13 +632,15 @@ object SnodeAPI {
} }
} }
private fun buildAlterTtlParams( // TODO: in future this will probably need to use the closed group subkeys / admin keys for group swarms private fun buildAlterTtlParams(
messageHashes: List<String>, messageHashes: List<String>,
newExpiry: Long, newExpiry: Long,
publicKey: String, publicKey: String,
signingKey: ByteArray,
pubKeyEd25519: String? = null,
extend: Boolean = false, extend: Boolean = false,
shorten: Boolean = false): Map<String, Any>? { shorten: Boolean = false): Map<String, Any>? {
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
val params = mutableMapOf( val params = mutableMapOf(
"expiry" to newExpiry, "expiry" to newExpiry,
"messages" to messageHashes, "messages" to messageHashes,
@@ -619,21 +654,23 @@ object SnodeAPI {
val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray() val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray()
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES) val signature = ByteArray(Sign.BYTES)
try { try {
sodium.cryptoSignDetached( sodium.cryptoSignDetached(
signature, signature,
signData, signData,
signData.size.toLong(), signData.size.toLong(),
userEd25519KeyPair.secretKey.asBytes signingKey
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Signing data failed with user secret key", e) Log.e("Loki", "Signing data failed with user secret key", e)
return null return null
} }
params["pubkey"] = publicKey params["pubkey"] = publicKey
params["pubkey_ed25519"] = ed25519PublicKey if (pubKeyEd25519 != null) {
params["pubkey_ed25519"] = pubKeyEd25519
}
params["signature"] = Base64.encodeBytes(signature) params["signature"] = Base64.encodeBytes(signature)
return params return params