diff --git a/app/build.gradle b/app/build.gradle
index 56f2a4ce1..333031759 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 deab87dd6..be464ad75 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 000000000..04b8dab95
--- /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 a20a3a2a6..822564b4f 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 59cb8ede0..e58f1db5f 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 000000000..e7a3ce107
--- /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 000000000..7b02efad6
--- /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 9a5cba044..aa5e7a14d 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 cb9392f69..b64936bea 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 8d1e2dd23..dd28bef23 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 f4e267279..2534ac160 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 eb5313ae1..924c3e6ae 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 {