mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 04:57:48 +00:00
feat: add background group creation and placeholder spinny progress
This commit is contained in:
@@ -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 = {},
|
||||
|
@@ -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()
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
@@ -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()
|
||||
|
@@ -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 ->
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user