From e79a980f2cec0366e0063cedb8c0cf22144cba3d Mon Sep 17 00:00:00 2001
From: 0x330a <92654767+0x330a@users.noreply.github.com>
Date: Thu, 19 Oct 2023 17:18:56 +1100
Subject: [PATCH] refactor: move test functions to automation and storage
utilities, add compose component activity dependency and create group
fragment content descriptions, add compose only testing for create group
logic, update protos to match latest for chunk 2
---
app/build.gradle | 6 +-
app/src/androidTest/AndroidManifest.xml | 6 +-
.../loki/messenger/CreateGroupTests.kt | 63 ++++++++++++++
.../loki/messenger/HomeActivityTests.kt | 81 ++++--------------
.../network/loki/messenger/LibSessionTests.kt | 31 ++-----
.../loki/messenger/util/LoginAutomation.kt | 82 +++++++++++++++++++
.../loki/messenger/util/StorageUtility.kt | 31 +++++++
.../securesms/groups/CreateGroupFragment.kt | 25 +++++-
app/src/main/res/values/ids.xml | 5 ++
app/src/main/res/values/strings.xml | 3 +
.../libsession_util/InstrumentedTests.kt | 47 +++++++++++
libsignal/protobuf/SignalService.proto | 81 ++++++++++++++++++
12 files changed, 364 insertions(+), 97 deletions(-)
create mode 100644 app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt
create mode 100644 app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt
create mode 100644 app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt
diff --git a/app/build.gradle b/app/build.gradle
index 56f2a4ce1c..333031759d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -299,9 +299,9 @@ dependencies {
implementation "com.opencsv:opencsv:4.6"
testImplementation "junit:junit:$junitVersion"
testImplementation 'org.assertj:assertj-core:3.11.1'
- testImplementation "org.mockito:mockito-inline:4.10.0"
+ testImplementation "org.mockito:mockito-inline:4.11.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
- androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3'
+ androidTestImplementation "org.mockito:mockito-android:4.11.0"
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "androidx.test:core:$testCoreVersion"
testImplementation "androidx.arch.core:core-testing:2.2.0"
@@ -330,6 +330,8 @@ 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"
androidTestUtil 'androidx.test:orchestrator:1.4.2'
testImplementation 'org.robolectric:robolectric:4.4'
diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml
index deab87dd62..be464ad75a 100644
--- a/app/src/androidTest/AndroidManifest.xml
+++ b/app/src/androidTest/AndroidManifest.xml
@@ -1,8 +1,10 @@
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+
+
\ No newline at end of file
diff --git a/app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt b/app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt
new file mode 100644
index 0000000000..04b8dab95b
--- /dev/null
+++ b/app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt
@@ -0,0 +1,63 @@
+package network.loki.messenger
+
+import androidx.compose.ui.test.hasContentDescriptionExactly
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import org.hamcrest.CoreMatchers.*
+import org.hamcrest.MatcherAssert.*
+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.ui.AppTheme
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreateGroupTests {
+
+ @get:Rule
+ val composeTest = createComposeRule()
+
+ @Test
+ fun testNavigateToCreateGroup() {
+ val application = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ // Accessibility IDs
+ val nameDesc = application.getString(R.string.AccessibilityId_closed_group_edit_group_name)
+ val buttonDesc = application.getString(R.string.AccessibilityId_create_closed_group_create_button)
+
+ lateinit var postedGroup: CreateGroupState
+ var backPressed = false
+ var closePressed = false
+
+ composeTest.setContent {
+ AppTheme {
+ CreateGroup(
+ viewState = CreateGroupFragment.ViewState.DEFAULT,
+ createGroupState = CreateGroupState("", "", emptySet()),
+ onCreate = { submitted ->
+ postedGroup = submitted
+ },
+ onBack = { backPressed = true },
+ onClose = { closePressed = true })
+ }
+ }
+
+ with(composeTest) {
+ onNode(hasContentDescriptionExactly(nameDesc)).performTextInput("Name")
+ onNode(hasContentDescriptionExactly(buttonDesc)).performClick()
+ }
+
+ assertThat(postedGroup.groupName, equalTo("Name"))
+ assertThat(backPressed, equalTo(false))
+ assertThat(closePressed, equalTo(false))
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
index a20a3a2a67..822564b4ff 100644
--- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
+++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt
@@ -1,14 +1,10 @@
package network.loki.messenger
-import android.Manifest
import android.app.Instrumentation
import android.content.ClipboardManager
import android.content.Context
-import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
-import androidx.test.espresso.UiController
-import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
@@ -20,11 +16,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
+import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
-import com.adevinta.android.barista.interaction.PermissionGranter
-import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
-import org.hamcrest.Matcher
+import network.loki.messenger.util.sendMessage
+import network.loki.messenger.util.setupLoggedInState
+import network.loki.messenger.util.waitFor
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
import org.junit.After
@@ -36,12 +32,10 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
-import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
import org.thoughtcrime.securesms.home.HomeActivity
-import org.thoughtcrime.securesms.mms.GlideApp
@RunWith(AndroidJUnit4::class)
-@LargeTest
+@SmallTest
class HomeActivityTests {
@get:Rule
@@ -59,38 +53,6 @@ class HomeActivityTests {
InstrumentationRegistry.getInstrumentation().removeMonitor(activityMonitor)
}
- private fun sendMessage(messageToSend: String, linkPreview: LinkPreview? = null) {
- // assume in chat activity
- onView(allOf(isDescendantOfA(withId(R.id.inputBar)),withId(R.id.inputBarEditText))).perform(ViewActions.replaceText(messageToSend))
- if (linkPreview != null) {
- val activity = activityMonitor.waitForActivity() as ConversationActivityV2
- val glide = GlideApp.with(activity)
- activity.findViewById(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
- }
- onView(allOf(isDescendantOfA(withId(R.id.inputBar)),inputButtonWithDrawable(R.drawable.ic_arrow_up))).perform(ViewActions.click())
- // TODO: text can flaky on cursor reload, figure out a better way to wait for the UI to settle with new data
- onView(isRoot()).perform(waitFor(500))
- }
-
- private fun setupLoggedInState(hasViewedSeed: Boolean = false) {
- // landing activity
- onView(withId(R.id.registerButton)).perform(ViewActions.click())
- // session ID - register activity
- onView(withId(R.id.registerButton)).perform(ViewActions.click())
- // display name selection
- onView(withId(R.id.displayNameEditText)).perform(ViewActions.typeText("test-user123"))
- onView(withId(R.id.registerButton)).perform(ViewActions.click())
- // PN select
- if (hasViewedSeed) {
- // has viewed seed is set to false after register activity
- TextSecurePreferences.setHasViewedSeed(InstrumentationRegistry.getInstrumentation().targetContext, true)
- }
- onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
- onView(withId(R.id.registerButton)).perform(ViewActions.click())
- // allow notification permission
- PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
- }
-
private fun goToMyChat() {
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
@@ -134,11 +96,13 @@ class HomeActivityTests {
setupLoggedInState()
goToMyChat()
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
- sendMessage("howdy")
- sendMessage("test")
- // tests url rewriter doesn't crash
- sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
- sendMessage("https://www.ámazon.com")
+ with (activityMonitor.waitForActivity() as ConversationActivityV2) {
+ sendMessage("howdy")
+ sendMessage("test")
+ // tests url rewriter doesn't crash
+ sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
+ sendMessage("https://www.ámazon.com")
+ }
}
@Test
@@ -148,7 +112,9 @@ class HomeActivityTests {
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
// given the link url text
val url = "https://www.ámazon.com"
- sendMessage(url, LinkPreview(url, "amazon", Optional.absent()))
+ with (activityMonitor.waitForActivity() as ConversationActivityV2) {
+ sendMessage(url, LinkPreview(url, "amazon", Optional.absent()))
+ }
// when the URL span is clicked
onView(withSubstring(url)).perform(ViewActions.click())
@@ -162,21 +128,4 @@ class HomeActivityTests {
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
}
- /**
- * Perform action of waiting for a specific time.
- */
- fun waitFor(millis: Long): ViewAction {
- return object : ViewAction {
- override fun getConstraints(): Matcher? {
- return isRoot()
- }
-
- override fun getDescription(): String = "Wait for $millis milliseconds."
-
- override fun perform(uiController: UiController, view: View?) {
- uiController.loopMainThreadForAtLeast(millis)
- }
- }
- }
-
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt
index 59cb8ede08..e58f1db5f2 100644
--- a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt
+++ b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt
@@ -9,12 +9,15 @@ import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.util.Contact
import network.loki.messenger.libsession_util.util.ExpiryMode
+import network.loki.messenger.util.applySpiedStorage
+import network.loki.messenger.util.maybeGetUserInfo
+import network.loki.messenger.util.randomSeedBytes
+import network.loki.messenger.util.randomSessionId
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argThat
import org.mockito.kotlin.eq
-import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.utilities.TextSecurePreferences
@@ -22,32 +25,15 @@ import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
-import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
@SmallTest
class LibSessionTests {
- private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
- private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
- private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
-
private var fakeHashI = 0
private val nextFakeHash: String
get() = "fakehash${fakeHashI++}"
- private fun maybeGetUserInfo(): Pair? {
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
- val prefs = appContext.prefs
- val localUserPublicKey = prefs.getLocalNumber()
- val secretKey = with(appContext) {
- val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
- edKey.secretKey.asBytes
- }
- return if (localUserPublicKey == null || secretKey == null) null
- else secretKey to localUserPublicKey
- }
-
private fun buildContactMessage(contactList: List): ByteArray {
val (key,_) = maybeGetUserInfo()!!
val contacts = Contacts.Companion.newInstance(key)
@@ -80,9 +66,8 @@ class LibSessionTests {
@Test
fun migration_one_to_ones() {
- val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
- val storageSpy = spy(app.storage)
- app.storage = storageSpy
+ val applicationContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val storage = applicationContext.applySpiedStorage()
val newContactId = randomSessionId()
val singleContact = Contact(
@@ -93,10 +78,10 @@ class LibSessionTests {
val newContactMerge = buildContactMessage(listOf(singleContact))
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
fakePollNewConfig(contacts, newContactMerge)
- verify(storageSpy).addLibSessionContacts(argThat {
+ verify(storage).addLibSessionContacts(argThat {
first().let { it.id == newContactId && it.approved } && size == 1
})
- verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
+ verify(storage).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
}
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt b/app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt
new file mode 100644
index 0000000000..e7a3ce107c
--- /dev/null
+++ b/app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt
@@ -0,0 +1,82 @@
+package network.loki.messenger.util
+
+import android.Manifest
+import android.view.View
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.platform.app.InstrumentationRegistry
+import com.adevinta.android.barista.interaction.PermissionGranter
+import network.loki.messenger.R
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers
+import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
+import org.session.libsession.utilities.TextSecurePreferences
+import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
+import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
+import org.thoughtcrime.securesms.mms.GlideApp
+
+fun setupLoggedInState(hasViewedSeed: Boolean = false) {
+ // landing activity
+ onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
+ // session ID - register activity
+ onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
+ // display name selection
+ onView(ViewMatchers.withId(R.id.displayNameEditText))
+ .perform(ViewActions.typeText("test-user123"))
+ onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
+ // PN select
+ if (hasViewedSeed) {
+ // has viewed seed is set to false after register activity
+ TextSecurePreferences.setHasViewedSeed(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ true
+ )
+ }
+ onView(ViewMatchers.withId(R.id.backgroundPollingOptionView))
+ .perform(ViewActions.click())
+ onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
+ // allow notification permission
+ PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
+}
+
+fun ConversationActivityV2.sendMessage(messageToSend: String, linkPreview: LinkPreview? = null) {
+ // assume in chat activity
+ onView(
+ Matchers.allOf(
+ ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.inputBar)),
+ ViewMatchers.withId(R.id.inputBarEditText)
+ )
+ ).perform(ViewActions.replaceText(messageToSend))
+ if (linkPreview != null) {
+ val glide = GlideApp.with(this)
+ this.findViewById(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
+ }
+ onView(
+ Matchers.allOf(
+ ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.inputBar)),
+ InputBarButtonDrawableMatcher.inputButtonWithDrawable(R.drawable.ic_arrow_up)
+ )
+ ).perform(ViewActions.click())
+ // TODO: text can flaky on cursor reload, figure out a better way to wait for the UI to settle with new data
+ onView(ViewMatchers.isRoot()).perform(waitFor(500))
+}
+
+/**
+ * Perform action of waiting for a specific time.
+ */
+fun waitFor(millis: Long): ViewAction {
+ return object : ViewAction {
+ override fun getConstraints(): Matcher? {
+ return ViewMatchers.isRoot()
+ }
+
+ override fun getDescription(): String = "Wait for $millis milliseconds."
+
+ override fun perform(uiController: UiController, view: View?) {
+ uiController.loopMainThreadForAtLeast(millis)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt b/app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt
new file mode 100644
index 0000000000..7b02efad68
--- /dev/null
+++ b/app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt
@@ -0,0 +1,31 @@
+package network.loki.messenger.util
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.mockito.kotlin.spy
+import org.session.libsignal.utilities.hexEncodedPublicKey
+import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.crypto.KeyPairUtilities
+import org.thoughtcrime.securesms.database.Storage
+import kotlin.random.Random
+
+fun maybeGetUserInfo(): Pair? {
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
+ val prefs = appContext.prefs
+ val localUserPublicKey = prefs.getLocalNumber()
+ val secretKey = with(appContext) {
+ val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
+ edKey.secretKey.asBytes
+ }
+ return if (localUserPublicKey == null || secretKey == null) null
+ else secretKey to localUserPublicKey
+}
+
+fun ApplicationContext.applySpiedStorage(): Storage {
+ val storageSpy = spy(storage)!!
+ storage = storageSpy
+ return storageSpy
+}
+
+fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
+fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
+fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
index 9a5cba044c..aa5e7a14de 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt
@@ -35,6 +35,8 @@ 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.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
@@ -170,33 +172,46 @@ fun CreateGroup(
.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),
+ .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),
+ .padding(vertical = 8.dp, horizontal = 24.dp)
+ .semantics {
+ contentDescription = descriptionDescription
+ },
)
// Group list
MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp))
}
// 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),
+ .padding(16.dp)
+ .semantics {
+ contentDescription = createDescription
+ }
+ ,
shape = RoundedCornerShape(32.dp)
) {
Text(
@@ -209,7 +224,9 @@ fun CreateGroup(
}
}
if (viewState.isLoading) {
- Box(modifier = modifier.fillMaxSize().background(Color.Gray.copy(alpha = 0.5f))) {
+ Box(modifier = modifier
+ .fillMaxSize()
+ .background(Color.Gray.copy(alpha = 0.5f))) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index cb9392f697..b64936bea8 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -3,4 +3,9 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8d1e2dd230..dd28bef23f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -80,6 +80,9 @@
Done
Mentions list
Contact mentions
+ Group name
+ Group description
+ Create group
Call button
Settings
diff --git a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt
index f4e2672791..2534ac1605 100644
--- a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt
+++ b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt
@@ -702,4 +702,51 @@ class InstrumentedTests {
assertThat(groupInfo.getDescription(), equalTo("This is a test group"))
}
+ @Test
+ fun testGroupKeyConfig() {
+ val (userPubKey, userSecret) = keyPair
+ val groupConfig = UserGroupsConfig.newInstance(userSecret)
+ val group = groupConfig.createGroup()
+ groupConfig.set(group)
+ val groupInfo = GroupInfoConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey())
+ groupInfo.setName("test")
+ val groupMembers = GroupMembersConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey())
+ groupMembers.set(
+ GroupMember(
+ sessionId = SessionId(IdPrefix.STANDARD, Sodium.ed25519PkToCurve25519(userPubKey)).hexString(),
+ name = "admin",
+ admin = true
+ )
+ )
+ val membersDump = groupMembers.dump()
+ val infoDump = groupInfo.dump()
+
+ val ourKeyConfig = GroupKeysConfig.newInstance(
+ userSecret,
+ group.groupSessionId.pubKeyBytes,
+ group.signingKey(),
+ info = groupInfo,
+ members = groupMembers
+ )
+
+ assertThat(ourKeyConfig.needsRekey(), equalTo(false))
+ val pushed = ourKeyConfig.pendingConfig()!!
+ val messageTimestamp = System.currentTimeMillis()
+ ourKeyConfig.loadKey(pushed, "testabc", messageTimestamp, groupInfo, groupMembers)
+ assertThat(ourKeyConfig.needsDump(), equalTo(true))
+ ourKeyConfig.dump()
+ assertThat(ourKeyConfig.needsRekey(), equalTo(false))
+ val mergeInfo = GroupInfoConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey(), infoDump)
+ val mergeMembers = GroupMembersConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey(), membersDump)
+ val mergeConfig = GroupKeysConfig.newInstance(userSecret, group.groupSessionId.pubKeyBytes, group.signingKey(), info = mergeInfo, members = mergeMembers)
+ mergeConfig.loadKey(pushed, "testabc", messageTimestamp, mergeInfo, mergeMembers)
+ assertThat(mergeConfig.needsRekey(), equalTo(false))
+ assertThat(mergeConfig.keys().size, equalTo(1))
+ assertThat(ourKeyConfig.keys().size, equalTo(1))
+ assertThat(mergeConfig.keys().first(), equalTo(ourKeyConfig.keys().first()))
+ assertThat(ourKeyConfig.groupKeys().size, equalTo(1))
+ assertThat(mergeConfig.groupKeys().size, equalTo(1))
+ assertThat(ourKeyConfig.groupKeys().first(), equalTo(mergeConfig.groupKeys().first()))
+ }
+
}
\ No newline at end of file
diff --git a/libsignal/protobuf/SignalService.proto b/libsignal/protobuf/SignalService.proto
index eb5313ae12..924c3e6aec 100644
--- a/libsignal/protobuf/SignalService.proto
+++ b/libsignal/protobuf/SignalService.proto
@@ -127,6 +127,87 @@ message DataMessage {
optional GroupPromoteMessage promoteMessage = 34;
}
+ // New closed group update messages
+ message GroupUpdateMessage {
+ optional GroupUpdateInviteMessage inviteMessage = 1;
+ optional GroupUpdateDeleteMessage deleteMessage = 2;
+ optional GroupUpdateInfoChangeMessage infoChangeMessage = 3;
+ optional GroupUpdateMemberChangeMessage memberChangeMessage = 4;
+ optional GroupUpdatePromoteMessage promoteMessage = 5;
+ optional GroupUpdateMemberLeftMessage memberLeftMessage = 6;
+ optional GroupUpdateInviteResponseMessage inviteResponse = 7;
+ optional GroupUpdateDeleteMemberContentMessage deleteMemberContent = 8;
+ }
+
+ // New closed groups
+ message GroupUpdateInviteMessage {
+ // @required
+ required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
+ // @required
+ required string name = 2;
+ // @required
+ required bytes memberAuthData = 3;
+ optional bytes profileKey = 4;
+ optional LokiProfile profile = 5;
+ // @required
+ required bytes adminSignature = 6;
+ }
+
+ message GroupUpdateDeleteMessage {
+ // @required
+ required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
+ // @required
+ required bytes adminSignature = 2;
+ }
+
+ message GroupUpdatePromoteMessage {
+ // @required
+ required bytes groupIdentitySeed = 1;
+ }
+
+ message GroupUpdateInfoChangeMessage {
+ enum Type {
+ NAME = 1;
+ AVATAR = 2;
+ DISAPPEARING_MESSAGES = 3;
+ }
+
+ // @required
+ required Type type = 1;
+ optional string updatedName = 2;
+ optional uint32 updatedExpiration = 3;
+ }
+
+ message GroupUpdateMemberChangeMessage {
+ enum Type {
+ ADDED = 1;
+ REMOVED = 2;
+ PROMOTED = 3;
+ }
+
+ // @required
+ required Type type = 1;
+ repeated bytes memberPublicKeys = 2;
+ }
+
+ message GroupUpdateMemberLeftMessage {
+ // the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop)
+ }
+
+ message GroupUpdateInviteResponseMessage {
+ // @required
+ required bool isApproved = 1; // Whether the request was approved
+ optional bytes profileKey = 2;
+ optional LokiProfile profile = 3;
+ }
+
+ message GroupUpdateDeleteMemberContentMessage {
+ repeated bytes memberPublicKeys = 1;
+ // @required
+ required bytes adminSignature = 2;
+ }
+
+
message ClosedGroupControlMessage {
enum Type {