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.ViewGroup
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
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.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
@@ -28,6 +32,7 @@ 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.res.stringResource
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.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.utilities.Device
@@ -93,9 +101,13 @@ class CreateGroupFragment : Fragment() {
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 = {
@@ -110,11 +122,10 @@ class CreateGroupFragment : Fragment() {
data class ViewState(
val isLoading: Boolean,
@StringRes val error: Int?,
val createdThreadId: Long?
@StringRes val error: Int?
) {
companion object {
val DEFAULT = ViewState(false, null, null)
val DEFAULT = ViewState(false, null)
}
}
@@ -141,6 +152,7 @@ fun CreateGroup(
val scrollState = rememberScrollState()
Box {
Column(
modifier
.fillMaxWidth()) {
@@ -196,6 +208,14 @@ fun CreateGroup(
)
}
}
if (viewState.isLoading) {
Box(modifier = modifier.fillMaxSize().background(Color.Gray.copy(alpha = 0.5f))) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@@ -226,7 +246,7 @@ fun ClosedGroupPreview(
) {
PreviewTheme(themeResId) {
CreateGroup(
viewState = CreateGroupFragment.ViewState(false, null, null),
viewState = CreateGroupFragment.ViewState(false, null),
createGroupState = CreateGroupState("Group Name", "Test Group Description", emptySet()),
onCreate = {},
onClose = {},

View File

@@ -41,16 +41,16 @@ class CreateGroupViewModel @Inject constructor(
}
fun tryCreateGroup(createGroupState: CreateGroupState): Recipient? {
_viewState.postValue(CreateGroupFragment.ViewState(true, null, null))
_viewState.postValue(CreateGroupFragment.ViewState(true, null))
val name = createGroupState.groupName
val description = createGroupState.groupDescription
val members = createGroupState.members
// do some validations
// do some validation
if (name.isEmpty()) {
_viewState.postValue(
CreateGroupFragment.ViewState(false, R.string.error, null)
CreateGroupFragment.ViewState(false, R.string.error)
)
return null
}
@@ -58,6 +58,9 @@ class CreateGroupViewModel @Inject constructor(
// make a group
val newGroup = storage.createNewGroup(name, description, members) // TODO: handle optional
if (!newGroup.isPresent) {
_viewState.postValue(CreateGroupFragment.ViewState(isLoading = false, null))
}
return newGroup.orNull()
}

View File

@@ -198,3 +198,20 @@ Java_network_loki_messenger_libsession_1util_GroupKeysConfig_keys(JNIEnv *env, j
}
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) {
companion object {
@@ -322,6 +323,7 @@ class GroupKeysConfig(pointer: Long): ConfigSig(pointer) {
external fun needsRekey(): Boolean
external fun pendingKey(): ByteArray?
external fun pendingConfig(): ByteArray?
external fun currentHashes(): List<String>
external fun rekey(info: GroupInfoConfig, members: GroupMembersConfig): ByteArray
override fun close() {
free()

View File

@@ -23,6 +23,7 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.SessionId
import org.session.libsignal.utilities.Snode
import kotlin.time.Duration.Companion.days
class ClosedGroupPoller(private val executor: CoroutineScope,
private val closedGroupSessionId: SessionId,
@@ -97,6 +98,12 @@ class ClosedGroupPoller(private val executor: CoroutineScope,
val members = configFactoryProtocol.getGroupMemberConfig(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 infoIndex = 1
val membersIndex = 2
@@ -133,14 +140,26 @@ class ClosedGroupPoller(private val executor: CoroutineScope,
group.signingKey()
) ?: 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(
snode,
closedGroupSessionId.hexString(),
listOf(keysPoll, infoPoll, membersPoll, messagePoll)
requests
).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 (ENABLE_LOGGING) Log.d("ClosedGroupPoller", "Poll results @${SnodeAPI.nowWithOffset}:")
(pollResult["results"] as List<RawResponse>).forEachIndexed { index, response ->

View File

@@ -525,9 +525,11 @@ object SnodeAPI {
messageHashes: List<String>,
newExpiry: Long,
publicKey: String,
signingKey: ByteArray,
pubKeyEd25519: String? = null,
shorten: Boolean = false,
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(
Snode.Method.Expire.rawValue,
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 {
val parameters = mutableMapOf<String, Any>(
"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) {
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(
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>,
newExpiry: Long,
publicKey: String,
signingKey: ByteArray,
pubKeyEd25519: String? = null,
extend: Boolean = false,
shorten: Boolean = false): Map<String, Any>? {
val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
val params = mutableMapOf(
"expiry" to newExpiry,
"messages" to messageHashes,
@@ -619,21 +654,23 @@ object SnodeAPI {
val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray()
val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(
signature,
signData,
signData.size.toLong(),
userEd25519KeyPair.secretKey.asBytes
signingKey
)
} catch (e: Exception) {
Log.e("Loki", "Signing data failed with user secret key", e)
return null
}
params["pubkey"] = publicKey
params["pubkey_ed25519"] = ed25519PublicKey
if (pubKeyEd25519 != null) {
params["pubkey_ed25519"] = pubKeyEd25519
}
params["signature"] = Base64.encodeBytes(signature)
return params