diff --git a/.github/workflows/benchmarks-check.yml b/.github/workflows/benchmarks-check.yml
index 91941fd98f9..336eaa3ac78 100644
--- a/.github/workflows/benchmarks-check.yml
+++ b/.github/workflows/benchmarks-check.yml
@@ -59,7 +59,7 @@ jobs:
unset REPORT_FULL_PATH_REF
- name: Install bencher
- uses: bencherdev/bencher@v0.4.17
+ uses: bencherdev/bencher@main
- name: Run Bencher on PR
if: github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'
diff --git a/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml b/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml
index f6dbeda7d91..9dfb71c505a 100644
--- a/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml
+++ b/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml
@@ -43,7 +43,7 @@ jobs:
fetch-depth: 0
- name: Cherry pick to `develop`
- uses: wireapp/action-auto-cherry-pick@v1.0.0
+ uses: wireapp/action-auto-cherry-pick@v1.0.2
with:
target-branch: develop
pr-title-suffix: '🍒'
diff --git a/.github/workflows/generate-dokka.yml b/.github/workflows/generate-dokka.yml
index 30c7414c965..01e2e6e5aaa 100644
--- a/.github/workflows/generate-dokka.yml
+++ b/.github/workflows/generate-dokka.yml
@@ -25,7 +25,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy docs 🚀
- uses: JamesIves/github-pages-deploy-action@v4.2.5
+ uses: JamesIves/github-pages-deploy-action@v4.6.9
with:
branch: gh-pages
clean: false
diff --git a/.github/workflows/gradle-android-instrumented-tests.yml b/.github/workflows/gradle-android-instrumented-tests.yml
index 365b30453cc..434a797d9b9 100644
--- a/.github/workflows/gradle-android-instrumented-tests.yml
+++ b/.github/workflows/gradle-android-instrumented-tests.yml
@@ -104,7 +104,7 @@ jobs:
./**/build/outputs/androidTest-results/**/*.xml
- name: Publish Unit Test Results
- uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0
+ uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1
if: always()
with:
files: |
@@ -125,7 +125,7 @@ jobs:
steps:
- name: Download tests results
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4.1.7
continue-on-error: true
with:
name: test-results
diff --git a/.github/workflows/gradle-android-unit-tests.yml b/.github/workflows/gradle-android-unit-tests.yml
index 409fcd50812..672144cd5aa 100644
--- a/.github/workflows/gradle-android-unit-tests.yml
+++ b/.github/workflows/gradle-android-unit-tests.yml
@@ -56,7 +56,7 @@ jobs:
./**/build/test-results/**/*.xml
- name: Publish Unit Test Results
- uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0
+ uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1
if: always()
with:
files: |
@@ -77,7 +77,7 @@ jobs:
steps:
- name: Download tests results
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v4.1.7
continue-on-error: true
with:
name: test-results
diff --git a/.github/workflows/gradle-ios-tests.yml b/.github/workflows/gradle-ios-tests.yml
index 4693d88411a..8745f8bcc8b 100644
--- a/.github/workflows/gradle-ios-tests.yml
+++ b/.github/workflows/gradle-ios-tests.yml
@@ -60,7 +60,7 @@ jobs:
./**/build/test-results/**/*.xml
- name: Publish Unit Test Results
- uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0
+ uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1
if: always()
with:
files: |
@@ -81,7 +81,7 @@ jobs:
steps:
- name: Download tests results
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v4.1.7
continue-on-error: true
with:
name: test-results
diff --git a/.github/workflows/gradle-jvm-tests.yml b/.github/workflows/gradle-jvm-tests.yml
index 6335dd09489..295aefb1da5 100644
--- a/.github/workflows/gradle-jvm-tests.yml
+++ b/.github/workflows/gradle-jvm-tests.yml
@@ -80,14 +80,14 @@ jobs:
sudo apt-get install -y python3-pip
- name: Publish Unit Test Results
- uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0
+ uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1
if: always()
with:
files: |
**/build/test-results/**/*.xml
- name: Upload test report to codecov
- uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457
+ uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: "build/reports/kover/report.xml"
@@ -112,7 +112,7 @@ jobs:
steps:
- name: Download tests results
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v4.1.7
continue-on-error: true
- name: Display structure of downloaded files
run: ls -R
diff --git a/.github/workflows/semantic-commit-lint.yml b/.github/workflows/semantic-commit-lint.yml
index 88783c2b437..eab76bf3322 100644
--- a/.github/workflows/semantic-commit-lint.yml
+++ b/.github/workflows/semantic-commit-lint.yml
@@ -19,7 +19,7 @@ jobs:
# Please look up the latest version from
# https://github.com/amannn/action-semantic-pull-request/releases
- name: Run Semantic Commint Linter
- uses: amannn/action-semantic-pull-request@v5.5.2
+ uses: amannn/action-semantic-pull-request@v5.5.3
with:
# Configure which types are allowed.
# Default: https://github.com/commitizen/conventional-commit-types
diff --git a/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt b/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt
index 3819da44213..7d24aa9b05e 100644
--- a/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt
+++ b/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt
@@ -25,7 +25,6 @@ import com.wire.kalium.logger.KaliumLogLevel
import com.wire.kalium.logic.CoreLogger
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.featureFlags.KaliumConfigs
-import com.wire.kalium.logic.sync.ForegroundNotificationDetailsProvider
import com.wire.kalium.logic.sync.WrapperWorkerFactory
import java.io.File
@@ -50,13 +49,7 @@ class KaliumApplication : Application(), Configuration.Provider {
override val workManagerConfiguration: Configuration
get() {
- val myWorkerFactory = WrapperWorkerFactory(
- coreLogic,
- object : ForegroundNotificationDetailsProvider {
- override fun getSmallIconResId(): Int = R.drawable.ic_launcher_foreground
- }
- )
-
+ val myWorkerFactory = WrapperWorkerFactory(coreLogic) { R.drawable.ic_launcher_foreground }
return Configuration.Builder()
.setWorkerFactory(myWorkerFactory)
.build()
diff --git a/build.gradle.kts b/build.gradle.kts
index aff4e98c0e8..c5387e7e8e2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -143,3 +143,82 @@ moduleGraphConfig {
readmePath.set("./README.md")
heading.set("#### Dependency Graph")
}
+
+tasks.register("runAllUnitTests") {
+ description = "Runs all Unit Tests."
+
+ rootProject.subprojects {
+ if (tasks.findByName("testDebugUnitTest") != null) {
+ println("Adding $name to runUnitTests")
+ dependsOn(":$name:testDebugUnitTest")
+ }
+ if (name != "cryptography") {
+ if (tasks.findByName("jvmTest") != null) {
+ println("Adding $name to jvmTest")
+ dependsOn(":$name:jvmTest")
+ }
+ }
+ }
+}
+
+tasks.register("aggregateTestResults") {
+ description = "Aggregates all Unit Test results into a single report."
+
+ doLast {
+ val testResultsDir = rootProject.layout.buildDirectory.dir("testResults").get().asFile
+ testResultsDir.deleteRecursively()
+ testResultsDir.mkdirs()
+
+ val indexHtmlFile = File(testResultsDir, "index.html")
+ indexHtmlFile.writeText(
+ """
+
+
+ Aggregated Test Reports
+
+
+ Aggregated Test Reports
+
+ """.trimIndent()
+ )
+
+ rootProject.subprojects {
+ val testResultsParentDir = layout.buildDirectory.dir("reports/tests").get().asFile
+
+ if (testResultsParentDir.exists()) {
+ testResultsParentDir.listFiles()?.forEach { testDir ->
+ if (testDir.isDirectory) {
+ val subprojectDir = File(testResultsDir, "$name/${testDir.name}")
+ subprojectDir.mkdirs()
+
+ testDir.copyRecursively(subprojectDir, overwrite = true)
+
+ indexHtmlFile.appendText(
+ """
+ - $name - ${testDir.name} Report
+ """.trimIndent()
+ )
+ }
+ }
+ }
+ }
+
+ indexHtmlFile.appendText(
+ """
+
+
+
+ """.trimIndent()
+ )
+
+ // Print the location of the aggregated test results directory
+ // relative to the current terminal working dir
+ val currentWorkingDir = File(System.getProperty("user.dir"))
+ val relativePath = testResultsDir.relativeTo(currentWorkingDir).path
+ println("Aggregated test reports are available at: $relativePath")
+ }
+}
+
+tasks.wrapper {
+ distributionType = Wrapper.DistributionType.ALL
+}
diff --git a/calling/build.gradle.kts b/calling/build.gradle.kts
index 2851cc4187d..b3587ad4842 100644
--- a/calling/build.gradle.kts
+++ b/calling/build.gradle.kts
@@ -67,3 +67,15 @@ kotlin {
}
}
}
+
+android {
+ defaultConfig {
+ ndk {
+ abiFilters.apply {
+ add("armeabi-v7a")
+ add("arm64-v8a")
+ add("x86_64")
+ }
+ }
+ }
+}
diff --git a/calling/consumer-proguard-rules.pro b/calling/consumer-proguard-rules.pro
index eed5182980c..3825933e243 100644
--- a/calling/consumer-proguard-rules.pro
+++ b/calling/consumer-proguard-rules.pro
@@ -6,6 +6,9 @@
-keep class com.waz.call.CaptureDevice { *; }
-keep class com.waz.media.manager.** { *; }
-keep class com.waz.service.call.** { *; }
+-dontwarn org.webrtc.CalledByNative
+-dontwarn org.webrtc.JniCommon
+-dontwarn org.webrtc.audio.AudioDeviceModule
# Avs SoundLink
-keep class com.waz.soundlink.SoundLinkAPI { *; }
diff --git a/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt b/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt
index 479d858a0ab..242b1b5a27a 100644
--- a/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt
+++ b/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt
@@ -214,6 +214,6 @@ interface Calling : Library {
)
companion object {
- val INSTANCE by lazy { Native.load("avs", Calling::class.java)!! }
+ val INSTANCE: Calling by lazy { Native.load("avs", Calling::class.java)!! }
}
}
diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts
index 2aebe850ecb..244c656e822 100644
--- a/cli/build.gradle.kts
+++ b/cli/build.gradle.kts
@@ -75,6 +75,8 @@ kotlin {
implementation(libs.coroutines.core)
implementation(libs.ktxDateTime)
implementation(libs.mordant)
+ implementation(libs.ktxSerialization)
+ implementation(libs.ktxIO)
}
}
val jvmMain by getting {
diff --git a/cli/src/appleMain/kotlin/main.kt b/cli/src/appleMain/kotlin/main.kt
index a486a8d7298..fd6410e1da8 100644
--- a/cli/src/appleMain/kotlin/main.kt
+++ b/cli/src/appleMain/kotlin/main.kt
@@ -21,6 +21,7 @@ import com.wire.kalium.cli.CLIApplication
import com.wire.kalium.cli.commands.AddMemberToGroupCommand
import com.wire.kalium.cli.commands.CreateGroupCommand
import com.wire.kalium.cli.commands.DeleteClientCommand
+import com.wire.kalium.cli.commands.GenerateEventsCommand
import com.wire.kalium.cli.commands.ListenGroupCommand
import com.wire.kalium.cli.commands.LoginCommand
import com.wire.kalium.cli.commands.MarkAsReadCommand
@@ -39,6 +40,7 @@ fun main(args: Array) = CLIApplication().subcommands(
RefillKeyPackagesCommand(),
MarkAsReadCommand(),
InteractiveCommand(),
- UpdateSupportedProtocolsCommand()
+ UpdateSupportedProtocolsCommand(),
+ GenerateEventsCommand()
)
).main(args)
diff --git a/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/GenerateEventsCommand.kt b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/GenerateEventsCommand.kt
new file mode 100644
index 00000000000..c3adb8a5672
--- /dev/null
+++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/GenerateEventsCommand.kt
@@ -0,0 +1,99 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.cli.commands
+
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.PrintMessage
+import com.github.ajalt.clikt.core.requireObject
+import com.github.ajalt.clikt.parameters.arguments.argument
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.options.required
+import com.github.ajalt.clikt.parameters.types.int
+import com.wire.kalium.logic.data.conversation.ClientId
+import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.data.id.QualifiedClientID
+import com.wire.kalium.logic.data.user.UserId
+import com.wire.kalium.logic.feature.UserSessionScope
+import com.wire.kalium.logic.data.event.EventGenerator
+import com.wire.kalium.logic.functional.getOrFail
+import com.wire.kalium.network.api.authenticated.notification.NotificationResponse
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.runBlocking
+import kotlinx.datetime.Clock
+import kotlinx.io.Buffer
+import kotlinx.io.buffered
+import kotlinx.io.files.Path
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.io.files.SystemFileSystem
+import kotlinx.io.writeString
+
+class GenerateEventsCommand : CliktCommand(name = "generate-events") {
+
+ private val userSession by requireObject()
+ private val targetUserId: String by option(help = "Target User which events will be generator for.").required()
+ private val targetClientId: String by option(help = "Target Client which events will be generator for.").required()
+ private val conversationId: String by option(help = "Target conversation which which will receive the events").required()
+ private val eventLimit: Int by argument("Number of events to generate").int()
+ private val outputFile: String by argument("Output file for the generated events")
+
+ private var json = Json {
+ prettyPrint = true
+ }
+
+ override fun run() = runBlocking {
+ val selfUserId = userSession.users.getSelfUser().first().id
+ val selfClientId = userSession.clientIdProvider().getOrFail { throw PrintMessage("No self client is registered") }
+ val targetUserId = UserId(value = targetUserId, domain = selfUserId.domain)
+ val targetClientId = ClientId(targetClientId)
+
+ userSession.debug.establishSession(
+ userId = targetUserId,
+ clientId = targetClientId
+ )
+ val generator = EventGenerator(
+ selfClient = QualifiedClientID(
+ clientId = selfClientId,
+ userId = selfUserId
+ ),
+ targetClient = QualifiedClientID(
+ clientId = targetClientId,
+ userId = targetUserId
+ ),
+ proteusClient = userSession.proteusClientProvider.getOrCreate()
+ )
+ val events = generator.generateEvents(
+ limit = eventLimit,
+ conversationId = ConversationId(conversationId, domain = selfUserId.domain)
+ )
+ val response = NotificationResponse(
+ time = Clock.System.now().toString(),
+ hasMore = false,
+ notifications = events.toList()
+ )
+
+ val sink = SystemFileSystem.sink(Path(outputFile)).buffered()
+ val buffer = Buffer()
+ buffer.writeString(json.encodeToString(response))
+ sink.write(buffer, buffer.size)
+ sink.close()
+
+ echo("Generated $eventLimit event(s) written into $outputFile")
+ }
+}
diff --git a/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt b/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt
index 79404e19a8f..54c196bf100 100644
--- a/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt
+++ b/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt
@@ -26,6 +26,7 @@ import com.wire.kalium.cli.commands.ListenGroupCommand
import com.wire.kalium.cli.commands.LoginCommand
import com.wire.kalium.cli.commands.MarkAsReadCommand
import com.wire.kalium.cli.commands.ConsoleCommand
+import com.wire.kalium.cli.commands.GenerateEventsCommand
import com.wire.kalium.cli.commands.RefillKeyPackagesCommand
import com.wire.kalium.cli.commands.RemoveMemberFromGroupCommand
import com.wire.kalium.cli.commands.UpdateSupportedProtocolsCommand
@@ -40,6 +41,7 @@ fun main(args: Array) = CLIApplication().subcommands(
ConsoleCommand(),
RefillKeyPackagesCommand(),
MarkAsReadCommand(),
- UpdateSupportedProtocolsCommand()
+ UpdateSupportedProtocolsCommand(),
+ GenerateEventsCommand()
)
).main(args)
diff --git a/cryptography/build.gradle.kts b/cryptography/build.gradle.kts
index 3d86ef000c6..b4eda1de6ac 100644
--- a/cryptography/build.gradle.kts
+++ b/cryptography/build.gradle.kts
@@ -110,4 +110,13 @@ android {
testOptions.unitTests.all {
it.enabled = false
}
+ defaultConfig {
+ ndk {
+ abiFilters.apply {
+ add("armeabi-v7a")
+ add("arm64-v8a")
+ add("x86_64")
+ }
+ }
+ }
}
diff --git a/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
index 4414b91a749..3af5b2e36af 100644
--- a/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
+++ b/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
@@ -110,18 +110,24 @@ class ProteusClientCryptoBoxImpl constructor(
}
}
- override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray = lock.withLock {
+ override suspend fun decrypt(
+ message: ByteArray,
+ sessionId: CryptoSessionId,
+ handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T
+ ): T = lock.withLock {
withContext(defaultContext) {
val session = box.tryGetSession(sessionId.value)
wrapException {
if (session != null) {
val decryptedMessage = session.decrypt(message)
- session.save()
- decryptedMessage
+ handleDecryptedMessage(decryptedMessage).also {
+ session.save()
+ }
} else {
val result = box.initSessionFromMessage(sessionId.value, message)
- result.session.save()
- result.message
+ handleDecryptedMessage(result.message).also {
+ result.session.save()
+ }
}
}
}
@@ -181,9 +187,9 @@ class ProteusClientCryptoBoxImpl constructor(
try {
return b()
} catch (e: CryptoException) {
- throw ProteusException(e.message, fromCryptoException(e), e.cause)
+ throw ProteusException(e.message, fromCryptoException(e), e.code.ordinal, e.cause)
} catch (e: Exception) {
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause)
}
}
diff --git a/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt
index 27f355f6151..3e92d06cef7 100644
--- a/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt
+++ b/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt
@@ -31,7 +31,8 @@ import platform.Foundation.URLByAppendingPathComponent
@Suppress("TooManyFunctions")
class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: CoreCrypto) : ProteusClient {
@Suppress("EmptyFunctionBlock")
- override suspend fun close() {}
+ override suspend fun close() {
+ }
override fun getIdentity(): ByteArray {
return ByteArray(0)
@@ -72,18 +73,21 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co
wrapException { coreCrypto.proteusSessionFromPrekey(sessionId.value, toUByteList(preKeyCrypto.encodedData.decodeBase64Bytes())) }
}
- override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray {
+ override suspend fun decrypt(
+ message: ByteArray,
+ sessionId: CryptoSessionId,
+ handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T
+ ): T {
val sessionExists = doesSessionExist(sessionId)
return wrapException {
- if (sessionExists) {
- val decryptedMessage = toByteArray(coreCrypto.proteusDecrypt(sessionId.value, toUByteList(message)))
- coreCrypto.proteusSessionSave(sessionId.value)
- decryptedMessage
+ val decryptedMessage = if (sessionExists) {
+ toByteArray(coreCrypto.proteusDecrypt(sessionId.value, toUByteList(message)))
} else {
- val decryptedMessage = toByteArray(coreCrypto.proteusSessionFromMessage(sessionId.value, toUByteList(message)))
+ toByteArray(coreCrypto.proteusSessionFromMessage(sessionId.value, toUByteList(message)))
+ }
+ handleDecryptedMessage(decryptedMessage).also {
coreCrypto.proteusSessionSave(sessionId.value)
- decryptedMessage
}
}
}
@@ -98,7 +102,10 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co
override suspend fun encryptBatched(message: ByteArray, sessionIds: List): Map {
return wrapException {
- coreCrypto.proteusEncryptBatched(sessionIds.map { it.value }, toUByteList((message))).mapNotNull { entry ->
+ coreCrypto.proteusEncryptBatched(
+ sessionId = sessionIds.map { it.value },
+ plaintext = toUByteList((message))
+ ).mapNotNull { entry ->
CryptoSessionId.fromEncodedString(entry.key)?.let { sessionId ->
sessionId to toByteArray(entry.value)
}
@@ -126,14 +133,14 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co
}
@Suppress("TooGenericExceptionCaught")
- private fun wrapException(b: () -> T): T {
+ private inline fun wrapException(b: () -> T): T {
try {
return b()
} catch (e: CryptoException) {
// TODO underlying proteus error is not exposed atm
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, null)
} catch (e: Exception) {
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, null)
}
}
@@ -187,9 +194,9 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co
coreCrypto.proteusInit()
return ProteusClientCoreCryptoImpl(coreCrypto)
} catch (e: CryptoException) {
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause)
} catch (e: Exception) {
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause)
}
}
}
diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt
index eda576f6d24..89c70247f9e 100644
--- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt
+++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt
@@ -33,9 +33,7 @@ actual suspend fun coreCryptoCentral(
File(rootDir).mkdirs()
val coreCrypto = coreCryptoDeferredInit(
path = path,
- key = databaseKey,
- ciphersuites = emptyList(),
- nbKeyPackage = null
+ key = databaseKey
)
coreCrypto.setCallbacks(Callbacks())
return CoreCryptoCentralImpl(
@@ -45,14 +43,13 @@ actual suspend fun coreCryptoCentral(
}
private class Callbacks : CoreCryptoCallbacks {
-
- override fun authorize(conversationId: ByteArray, clientId: ClientId): Boolean {
+ override suspend fun authorize(conversationId: ByteArray, clientId: ClientId): Boolean {
// We always return true because our BE is currently enforcing that this constraint is always true
return true
}
- override fun clientIsExistingGroupUser(
- conversationId: ConversationId,
+ override suspend fun clientIsExistingGroupUser(
+ conversationId: ByteArray,
clientId: ClientId,
existingClients: List,
parentConversationClients: List?
@@ -61,11 +58,7 @@ private class Callbacks : CoreCryptoCallbacks {
return true
}
- override fun userAuthorize(
- conversationId: ConversationId,
- externalClientId: ClientId,
- existingClients: List
- ): Boolean {
+ override suspend fun userAuthorize(conversationId: ByteArray, externalClientId: ClientId, existingClients: List): Boolean {
// We always return true because our BE is currently enforcing that this constraint is always true
return true
}
diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt
index 9fbcd75953e..fdc9bc2b69c 100644
--- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt
+++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt
@@ -22,8 +22,11 @@ import com.wire.crypto.CoreCrypto
import com.wire.crypto.CoreCryptoException
import com.wire.crypto.client.toByteArray
import com.wire.kalium.cryptography.exceptions.ProteusException
+import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException
import io.ktor.util.decodeBase64Bytes
import io.ktor.util.encodeBase64
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import java.io.File
@Suppress("TooManyFunctions")
@@ -31,6 +34,9 @@ class ProteusClientCoreCryptoImpl private constructor(
private val coreCrypto: CoreCrypto,
) : ProteusClient {
+ private val mutex = Mutex()
+ private val existingSessionsCache = mutableSetOf()
+
override suspend fun close() {
coreCrypto.close()
}
@@ -46,6 +52,7 @@ class ProteusClientCoreCryptoImpl private constructor(
override suspend fun remoteFingerPrint(sessionId: CryptoSessionId): ByteArray = wrapException {
coreCrypto.proteusFingerprintRemote(sessionId.value).toByteArray()
}
+
override suspend fun getFingerprintFromPreKey(preKey: PreKeyCrypto): ByteArray = wrapException {
coreCrypto.proteusFingerprintPrekeybundle(preKey.encodedData.decodeBase64Bytes()).toByteArray()
}
@@ -62,9 +69,16 @@ class ProteusClientCoreCryptoImpl private constructor(
return wrapException { toPreKey(coreCrypto.proteusLastResortPrekeyId().toInt(), coreCrypto.proteusLastResortPrekey()) }
}
- override suspend fun doesSessionExist(sessionId: CryptoSessionId): Boolean {
- return wrapException {
+ override suspend fun doesSessionExist(sessionId: CryptoSessionId): Boolean = mutex.withLock {
+ if (existingSessionsCache.contains(sessionId)) {
+ return@withLock true
+ }
+ wrapException {
coreCrypto.proteusSessionExists(sessionId.value)
+ }.also { exists ->
+ if (exists) {
+ existingSessionsCache.add(sessionId)
+ }
}
}
@@ -72,19 +86,20 @@ class ProteusClientCoreCryptoImpl private constructor(
wrapException { coreCrypto.proteusSessionFromPrekey(sessionId.value, preKeyCrypto.encodedData.decodeBase64Bytes()) }
}
- override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray {
+ override suspend fun decrypt(
+ message: ByteArray,
+ sessionId: CryptoSessionId,
+ handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T
+ ): T {
val sessionExists = doesSessionExist(sessionId)
return wrapException {
- if (sessionExists) {
- val decryptedMessage = coreCrypto.proteusDecrypt(sessionId.value, message)
- coreCrypto.proteusSessionSave(sessionId.value)
- decryptedMessage
+ val decryptedMessage = if (sessionExists) {
+ coreCrypto.proteusDecrypt(sessionId.value, message)
} else {
- val decryptedMessage = coreCrypto.proteusSessionFromMessage(sessionId.value, message)
- coreCrypto.proteusSessionSave(sessionId.value)
- decryptedMessage
+ coreCrypto.proteusSessionFromMessage(sessionId.value, message)
}
+ handleDecryptedMessage(decryptedMessage)
}
}
@@ -119,7 +134,8 @@ class ProteusClientCoreCryptoImpl private constructor(
}
}
- override suspend fun deleteSession(sessionId: CryptoSessionId) {
+ override suspend fun deleteSession(sessionId: CryptoSessionId) = mutex.withLock {
+ existingSessionsCache.remove(sessionId)
wrapException {
coreCrypto.proteusSessionDelete(sessionId.value)
}
@@ -130,9 +146,15 @@ class ProteusClientCoreCryptoImpl private constructor(
try {
return b()
} catch (e: CoreCryptoException) {
- throw ProteusException(e.message, ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), e)
+ val proteusLastErrorCode = coreCrypto.proteusLastErrorCode()
+ throw ProteusException(
+ e.message,
+ ProteusException.fromProteusCode(proteusLastErrorCode.toInt()),
+ proteusLastErrorCode.toInt(),
+ e
+ )
} catch (e: Exception) {
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e)
}
}
@@ -157,30 +179,43 @@ class ProteusClientCoreCryptoImpl private constructor(
acc && File(rootDir).resolve(file).deleteRecursively()
}
- private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) {
- if (cryptoBoxFilesExists(File(rootDir))) {
- kaliumLogger.i("migrating from crypto box at: $rootDir")
- coreCrypto.proteusCryptoboxMigrate(rootDir)
- kaliumLogger.i("migration successful")
-
- if (deleteCryptoBoxFiles(rootDir)) {
- kaliumLogger.i("successfully deleted old crypto box files")
- } else {
- kaliumLogger.e("Failed to deleted old crypto box files at $rootDir")
- }
- }
- }
-
- @Suppress("TooGenericExceptionCaught")
+ @Suppress("TooGenericExceptionCaught", "ThrowsCount")
suspend operator fun invoke(coreCrypto: CoreCrypto, rootDir: String): ProteusClientCoreCryptoImpl {
try {
migrateFromCryptoBoxIfNecessary(coreCrypto, rootDir)
coreCrypto.proteusInit()
return ProteusClientCoreCryptoImpl(coreCrypto)
+ } catch (exception: ProteusStorageMigrationException) {
+ throw exception
} catch (e: CoreCryptoException) {
- throw ProteusException(e.message, ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), e.cause)
+ throw ProteusException(
+ message = e.message,
+ code = ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()),
+ intCode = coreCrypto.proteusLastErrorCode().toInt(),
+ cause = e.cause
+ )
} catch (e: Exception) {
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause)
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) {
+ try {
+ if (cryptoBoxFilesExists(File(rootDir))) {
+ kaliumLogger.i("migrating from crypto box at: $rootDir")
+ coreCrypto.proteusCryptoboxMigrate(rootDir)
+ kaliumLogger.i("migration successful")
+
+ if (deleteCryptoBoxFiles(rootDir)) {
+ kaliumLogger.i("successfully deleted old crypto box files")
+ } else {
+ kaliumLogger.e("Failed to deleted old crypto box files at $rootDir")
+ }
+ }
+ } catch (exception: Exception) {
+ kaliumLogger.e("Failed to migrate from crypto box to core crypto, exception: $exception")
+ throw ProteusStorageMigrationException("Failed to migrate from crypto box at $rootDir", exception)
}
}
}
diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt
index e57fb8c4fe2..055ecc27841 100644
--- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt
+++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt
@@ -81,8 +81,18 @@ interface ProteusClient {
@Throws(ProteusException::class, CancellationException::class)
suspend fun createSession(preKeyCrypto: PreKeyCrypto, sessionId: CryptoSessionId)
+ /**
+ * Decrypts a message.
+ * In case of success, calls [handleDecryptedMessage] with the decrypted bytes.
+ * @throws ProteusException in case of failure
+ * @throws CancellationException
+ */
@Throws(ProteusException::class, CancellationException::class)
- suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray
+ suspend fun decrypt(
+ message: ByteArray,
+ sessionId: CryptoSessionId,
+ handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T
+ ): T
@Throws(ProteusException::class, CancellationException::class)
suspend fun encrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray
diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt
index ca85a709fd5..547d0deefa9 100644
--- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt
+++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt
@@ -18,9 +18,14 @@
package com.wire.kalium.cryptography.exceptions
-class ProteusException(message: String?, val code: Code, cause: Throwable? = null) : Exception(message, cause) {
+open class ProteusException(message: String?, val code: Code, val intCode: Int?, cause: Throwable? = null) : Exception(message, cause) {
- constructor(message: String?, code: Int, cause: Throwable? = null) : this(message, fromNativeCode(code), cause)
+ constructor(message: String?, code: Int, cause: Throwable? = null) : this(
+ message,
+ fromNativeCode(code),
+ code,
+ cause
+ )
enum class Code {
/**
@@ -149,6 +154,9 @@ class ProteusException(message: String?, val code: Code, cause: Throwable? = nul
}
companion object {
+
+ const val SESSION_NOT_FOUND_INT = 2
+
@Suppress("MagicNumber")
fun fromNativeCode(code: Int): Code {
return when (code) {
@@ -191,3 +199,6 @@ class ProteusException(message: String?, val code: Code, cause: Throwable? = nul
}
}
}
+
+class ProteusStorageMigrationException(override val message: String, val rootCause: Throwable? = null) :
+ ProteusException(message, Int.MIN_VALUE, null)
diff --git a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt
index ec4f95be564..a27624f024a 100644
--- a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt
+++ b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt
@@ -1,20 +1,20 @@
- /*
- * Wire
- * Copyright (C) 2024 Wire Swiss GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see http://www.gnu.org/licenses/.
- */
+/*
+* Wire
+* Copyright (C) 2024 Wire Swiss GmbH
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program. If not, see http://www.gnu.org/licenses/.
+*/
package com.wire.kalium.cryptography
@@ -93,7 +93,7 @@ class ProteusClientTest : BaseProteusClientTest() {
val message = "Hi Alice!"
val aliceKey = aliceClient.newPreKeys(0, 10).first()
val encryptedMessage = bobClient.encryptWithPreKey(message.encodeToByteArray(), aliceKey, aliceSessionId)
- val decryptedMessage = aliceClient.decrypt(encryptedMessage, bobSessionId)
+ val decryptedMessage = aliceClient.decrypt(encryptedMessage, bobSessionId) { it }
assertEquals(message, decryptedMessage.decodeToString())
}
@@ -105,11 +105,11 @@ class ProteusClientTest : BaseProteusClientTest() {
val aliceKey = aliceClient.newPreKeys(0, 10).first()
val message1 = "Hi Alice!"
val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId)
- aliceClient.decrypt(encryptedMessage1, bobSessionId)
+ aliceClient.decrypt(encryptedMessage1, bobSessionId) {}
val message2 = "Hi again Alice!"
val encryptedMessage2 = bobClient.encrypt(message2.encodeToByteArray(), aliceSessionId)
- val decryptedMessage2 = aliceClient.decrypt(encryptedMessage2, bobSessionId)
+ val decryptedMessage2 = aliceClient.decrypt(encryptedMessage2, bobSessionId) { it }
assertEquals(message2, decryptedMessage2.decodeToString())
}
@@ -124,10 +124,10 @@ class ProteusClientTest : BaseProteusClientTest() {
val aliceKey = aliceClient.newPreKeys(0, 10).first()
val message1 = "Hi Alice!"
val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId)
- aliceClient.decrypt(encryptedMessage1, bobSessionId)
+ aliceClient.decrypt(encryptedMessage1, bobSessionId) {}
val exception: ProteusException = assertFailsWith {
- aliceClient.decrypt(encryptedMessage1, bobSessionId)
+ aliceClient.decrypt(encryptedMessage1, bobSessionId) {}
}
assertEquals(ProteusException.Code.DUPLICATE_MESSAGE, exception.code)
}
@@ -188,8 +188,44 @@ class ProteusClientTest : BaseProteusClientTest() {
}
}
+ // TODO: Implement on CoreCrypto as well once it supports transactions
+ @IgnoreJS
+ @IgnoreJvm
+ @IgnoreIOS
+ @Test
+ fun givenNonEncryptedClient_whenThrowingDuringTransaction_thenShouldNotSaveSessionAndBeAbleToDecryptAgain() = runTest {
+ val aliceRef = createProteusStoreRef(alice.id)
+ val failedAliceClient = createProteusClient(aliceRef)
+ val bobClient = createProteusClient(createProteusStoreRef(bob.id))
+
+ val aliceKey = failedAliceClient.newPreKeys(0, 10).first()
+ val message1 = "Hi Alice!"
+
+ var decryptedCount = 0
+
+ val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId)
+ try {
+ failedAliceClient.decrypt(encryptedMessage1, bobSessionId) {
+ decryptedCount++
+ throw NullPointerException("")
+ }
+ } catch (ignore: Throwable) {
+ /** No-op **/
+ }
+ // Assume that the app crashed after decrypting but before saving session.
+ // Trying to decrypt again should succeed.
+
+ val secondAliceClient = createProteusClient(aliceRef)
+
+ val result = secondAliceClient.decrypt(encryptedMessage1, bobSessionId) { result ->
+ decryptedCount++
+ result
+ }
+ assertEquals(message1, result.decodeToString())
+ assertEquals(2, decryptedCount)
+ }
+
companion object {
val PROTEUS_DB_SECRET = ProteusDBSecret("secret")
}
-
}
diff --git a/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
index f07272f107e..cfa765f1e9c 100644
--- a/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
+++ b/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
@@ -80,7 +80,7 @@ class ProteusClientCryptoBoxImpl : ProteusClient {
if (preKey != null) {
return toPreKey(box.getIdentity().public_key, preKey)
} else {
- throw ProteusException("Local identity doesn't exist", ProteusException.Code.UNKNOWN_ERROR)
+ throw ProteusException("Local identity doesn't exist", ProteusException.Code.UNKNOWN_ERROR, null, null)
}
}
@@ -102,12 +102,13 @@ class ProteusClientCryptoBoxImpl : ProteusClient {
box.session_from_prekey(sessionId.value, preKeyBundle.toArrayBuffer()).await()
}
- override suspend fun decrypt(
+ override suspend fun decrypt(
message: ByteArray,
- sessionId: CryptoSessionId
- ): ByteArray {
+ sessionId: CryptoSessionId,
+ handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T
+ ): T {
val decryptedMessage = box.decrypt(sessionId.value, message.toArrayBuffer()).await()
- return Int8Array(decryptedMessage.buffer).unsafeCast()
+ return handleDecryptedMessage(Int8Array(decryptedMessage.buffer).unsafeCast())
}
override suspend fun encrypt(
diff --git a/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
index 33dbbde607a..125f41ed96b 100644
--- a/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
+++ b/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt
@@ -27,17 +27,13 @@ import java.util.Base64
import kotlin.coroutines.CoroutineContext
@Suppress("TooManyFunctions")
-class ProteusClientCryptoBoxImpl constructor(
+class ProteusClientCryptoBoxImpl(
rootDir: String
) : ProteusClient {
- private val path: String
+ private val path: String = rootDir
private lateinit var box: CryptoBox
- init {
- path = rootDir
- }
-
fun openOrCreate() {
val directory = File(path)
box = wrapException {
@@ -84,14 +80,18 @@ class ProteusClientCryptoBoxImpl constructor(
wrapException { box.encryptFromPreKeys(sessionId.value, toPreKey(preKeyCrypto), ByteArray(0)) }
}
- override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray {
- return wrapException { box.decrypt(sessionId.value, message) }
+ override suspend fun decrypt(
+ message: ByteArray,
+ sessionId: CryptoSessionId,
+ handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T
+ ): T = wrapException {
+ handleDecryptedMessage(box.decrypt(sessionId.value, message))
}
override suspend fun encrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray {
return wrapException {
box.encryptFromSession(sessionId.value, message)
- }?.let { it } ?: throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND)
+ } ?: throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND, ProteusException.SESSION_NOT_FOUND_INT)
}
override suspend fun encryptBatched(message: ByteArray, sessionIds: List): Map {
@@ -121,13 +121,13 @@ class ProteusClientCryptoBoxImpl constructor(
}
@Suppress("TooGenericExceptionCaught")
- private fun wrapException(b: () -> T): T {
+ private inline fun wrapException(b: () -> T): T {
try {
return b()
} catch (e: CryptoException) {
throw ProteusException(e.message, e.code.ordinal, e.cause)
} catch (e: Exception) {
- throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause)
+ throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause)
}
}
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt
index fb0adcd8b76..5bc04b0eee8 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt
@@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.call
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.data.id.QualifiedID
enum class CallStatus {
STARTED,
@@ -39,7 +40,7 @@ data class Call(
val isMuted: Boolean,
val isCameraOn: Boolean,
val isCbrEnabled: Boolean,
- val callerId: String,
+ val callerId: QualifiedID,
val conversationName: String?,
val conversationType: Conversation.Type,
val callerName: String?,
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt
index 7545545157e..0c2f3bb9b79 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt
@@ -18,21 +18,55 @@
package com.wire.kalium.logic.data.call
+import com.wire.kalium.util.serialization.LenientJsonSerializer
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.nullable
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
@Serializable
data class CallClient(
@SerialName("userid") val userId: String,
@SerialName("clientid") val clientId: String,
- @SerialName("in_subconv") val isMemberOfSubconversation: Boolean = false
+ @SerialName("in_subconv") val isMemberOfSubconversation: Boolean = false,
+ @SerialName("quality")
+ @Serializable(with = CallQuality.CallQualityAsIntSerializer::class)
+ val quality: CallQuality = CallQuality.LOW
)
@Serializable
data class CallClientList(
@SerialName("clients") val clients: List
) {
- // TODO(optimization): Use a shared Json instance instead of creating one every time.
- fun toJsonString(): String = Json { isLenient = true }.encodeToString(serializer(), this)
+ fun toJsonString(): String = LenientJsonSerializer.json.encodeToString(serializer(), this)
+}
+
+enum class CallQuality {
+ ANY,
+ LOW,
+ HIGH;
+
+ data object CallQualityAsIntSerializer : KSerializer {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("quality", PrimitiveKind.INT).nullable
+
+ override fun serialize(encoder: Encoder, value: CallQuality) {
+ encoder.encodeInt(value.ordinal)
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ override fun deserialize(decoder: Decoder): CallQuality {
+ val value = if (decoder.decodeNotNullMark()) decoder.decodeInt() else 0
+ return when (value) {
+ 1 -> LOW
+ 2 -> HIGH
+ else -> ANY
+ }
+ }
+ }
}
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt
index 203f744a4e6..dd306c7b382 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt
@@ -20,9 +20,11 @@ package com.wire.kalium.logic.data.call
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.OtherUserMinimized
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.data.user.type.UserType
+import kotlinx.datetime.Instant
data class CallMetadataProfile(
val data: Map
@@ -31,6 +33,7 @@ data class CallMetadataProfile(
}
data class CallMetadata(
+ val callerId: QualifiedID,
val isMuted: Boolean,
val isCameraOn: Boolean,
val isCbrEnabled: Boolean,
@@ -44,11 +47,12 @@ data class CallMetadata(
val maxParticipants: Int = 0, // Was used for tracking
val protocol: Conversation.ProtocolInfo,
val activeSpeakers: Map> = mapOf(),
- val users: List = listOf()
+ val users: List = listOf(),
+ val screenShareMetadata: CallScreenSharingMetadata = CallScreenSharingMetadata()
) {
fun getFullParticipants(): List = participants.map { participant ->
val user = users.firstOrNull { it.id == participant.userId }
- val isSpeaking = (activeSpeakers[participant.id]?.contains(participant.clientId) ?: false) && !participant.isMuted
+ val isSpeaking = (activeSpeakers[participant.userId]?.contains(participant.clientId) ?: false) && !participant.isMuted
Participant(
id = participant.id,
clientId = participant.clientId,
@@ -59,7 +63,19 @@ data class CallMetadata(
isSharingScreen = participant.isSharingScreen,
hasEstablishedAudio = participant.hasEstablishedAudio,
avatarAssetId = user?.completePicture,
- userType = user?.userType ?: UserType.NONE
+ userType = user?.userType ?: UserType.NONE,
+ accentId = user?.accentId ?: 0
)
}
}
+
+/**
+ * [activeScreenShares] - map of user ids that share screen with the start timestamp
+ * [completedScreenShareDurationInMillis] - total time of already ended screen shares in milliseconds
+ * [uniqueSharingUsers] - set of users that were sharing a screen at least once
+ */
+data class CallScreenSharingMetadata(
+ val activeScreenShares: Map = emptyMap(),
+ val completedScreenShareDurationInMillis: Long = 0L,
+ val uniqueSharingUsers: Set = emptySet()
+)
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt
index 83ff455e796..0a8e1cf43f1 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt
@@ -33,6 +33,7 @@ data class Participant(
val hasEstablishedAudio: Boolean,
val avatarAssetId: UserAssetId? = null,
val userType: UserType = UserType.NONE,
+ val accentId: Int
)
data class ParticipantMinimized(
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt
index 9e19076a14b..b07cf80f009 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt
@@ -30,12 +30,16 @@ import com.wire.kalium.logic.data.id.TeamId
import com.wire.kalium.logic.data.message.MessagePreview
import com.wire.kalium.logic.data.message.UnreadEventType
import com.wire.kalium.logic.data.mls.CipherSuite
+import com.wire.kalium.logic.data.mls.MLSPublicKeys
+import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.OtherUser
import com.wire.kalium.logic.data.user.User
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.data.user.type.UserType
import com.wire.kalium.util.serialization.toJsonElement
import kotlinx.datetime.Instant
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
import kotlin.time.Duration
/**
@@ -78,7 +82,8 @@ data class Conversation(
val archivedDateTime: Instant?,
val mlsVerificationStatus: VerificationStatus,
val proteusVerificationStatus: VerificationStatus,
- val legalHoldStatus: LegalHoldStatus
+ val legalHoldStatus: LegalHoldStatus,
+ val mlsPublicKeys: MLSPublicKeys? = null
) {
companion object {
@@ -255,11 +260,23 @@ data class Conversation(
fun toLogMap(): Map
}
- data class Member(val id: UserId, val role: Role) {
+ @Serializable
+ data class Member(
+ @SerialName("id") val id: UserId,
+ @SerialName("role") val role: Role
+ ) {
+
+ @Serializable
sealed class Role {
+
+ @Serializable
data object Member : Role()
+
+ @Serializable
data object Admin : Role()
- data class Unknown(val name: String) : Role()
+
+ @Serializable
+ data class Unknown(@SerialName("name") val name: String) : Role()
override fun toString(): String =
when (this) {
@@ -290,18 +307,15 @@ sealed class ConversationDetails(open val conversation: Conversation) {
override val conversation: Conversation,
val otherUser: OtherUser,
val userType: UserType,
- val unreadEventCount: UnreadEventCount,
- val lastMessage: MessagePreview?
+ val isFavorite: Boolean = false
) : ConversationDetails(conversation)
data class Group(
override val conversation: Conversation,
val hasOngoingCall: Boolean = false,
- val unreadEventCount: UnreadEventCount,
- val lastMessage: MessagePreview?,
val isSelfUserMember: Boolean,
- val isSelfUserCreator: Boolean,
- val selfRole: Conversation.Member.Role?
+ val selfRole: Conversation.Member.Role?,
+ val isFavorite: Boolean = false
// val isTeamAdmin: Boolean, TODO kubaz
) : ConversationDetails(conversation)
@@ -341,6 +355,64 @@ sealed class ConversationDetails(open val conversation: Conversation) {
)
}
+data class ConversationDetailsWithEvents(
+ val conversationDetails: ConversationDetails,
+ val unreadEventCount: UnreadEventCount = emptyMap(),
+ val lastMessage: MessagePreview? = null,
+ val hasNewActivitiesToShow: Boolean = false,
+)
+
+fun ConversationDetails.interactionAvailability(): InteractionAvailability {
+ val availability = when (this) {
+ is ConversationDetails.Connection -> InteractionAvailability.DISABLED
+ is ConversationDetails.Group -> {
+ if (isSelfUserMember) InteractionAvailability.ENABLED
+ else InteractionAvailability.NOT_MEMBER
+ }
+
+ is ConversationDetails.OneOne -> when {
+ otherUser.defederated -> InteractionAvailability.DISABLED
+ otherUser.deleted -> InteractionAvailability.DELETED_USER
+ otherUser.connectionStatus == ConnectionState.BLOCKED -> InteractionAvailability.BLOCKED_USER
+ conversation.legalHoldStatus == Conversation.LegalHoldStatus.DEGRADED ->
+ InteractionAvailability.LEGAL_HOLD
+
+ else -> InteractionAvailability.ENABLED
+ }
+
+ is ConversationDetails.Self, is ConversationDetails.Team -> InteractionAvailability.DISABLED
+ }
+ return availability
+}
+
+enum class InteractionAvailability {
+ /**User is able to send messages and make calls */
+ ENABLED,
+
+ /**Self user is no longer conversation member */
+ NOT_MEMBER,
+
+ /**Other user is blocked by self user */
+ BLOCKED_USER,
+
+ /**Other team member or public user has been removed */
+ DELETED_USER,
+
+ /**
+ * This indicates that the conversation is using a protocol that self user does not support.
+ */
+ UNSUPPORTED_PROTOCOL,
+
+ /**Conversation type doesn't support messaging */
+ DISABLED,
+
+ /**
+ * One of conversation members is under legal hold and self user is not able to interact with it.
+ * This applies to 1:1 conversations only when other member is a guest.
+ */
+ LEGAL_HOLD
+}
+
data class MembersInfo(val self: Conversation.Member, val otherMembers: List)
data class MemberDetails(val user: User, val role: Conversation.Member.Role)
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt
new file mode 100644
index 00000000000..f1ca2a7bf39
--- /dev/null
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt
@@ -0,0 +1,25 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.conversation
+
+enum class ConversationFilter {
+ ALL,
+ FAVORITES,
+ GROUPS,
+ ONE_ON_ONE
+}
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt
new file mode 100644
index 00000000000..675e9f5794f
--- /dev/null
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt
@@ -0,0 +1,38 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.conversation
+
+import com.wire.kalium.logic.data.id.QualifiedID
+
+data class ConversationFolder(
+ val id: String,
+ val name: String,
+ val type: FolderType
+)
+
+data class FolderWithConversations(
+ val id: String,
+ val name: String,
+ val type: FolderType,
+ val conversationIdList: List
+)
+
+enum class FolderType {
+ USER,
+ FAVORITE,
+}
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt
index f8b451f6eee..445bec974c1 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt
@@ -18,22 +18,29 @@
package com.wire.kalium.logic.data.conversation
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
/**
* Conversation muting settings type
*/
-sealed class MutedConversationStatus(open val status: Int = 0) {
+@Serializable
+sealed class MutedConversationStatus(@SerialName("status") open val status: Int = 0) {
/**
* 0 -> All notifications are displayed
*/
+ @Serializable
data object AllAllowed : MutedConversationStatus(0)
/**
* 1 -> Only mentions and replies are displayed (normal messages muted)
*/
+ @Serializable
data object OnlyMentionsAndRepliesAllowed : MutedConversationStatus(1)
/**
* 3 -> No notifications are displayed
*/
+ @Serializable
data object AllMuted : MutedConversationStatus(3)
}
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt
index a8f19d888f1..d6996ecb13f 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt
@@ -33,7 +33,8 @@ data class QualifiedID(
@SerialName("domain")
val domain: String
) {
- override fun toString(): String = if (domain.isEmpty()) value else "$value$VALUE_DOMAIN_SEPARATOR$domain"
+ override fun toString(): String =
+ if (domain.isEmpty()) value else "$value$VALUE_DOMAIN_SEPARATOR$domain"
fun toLogString(): String = if (domain.isEmpty()) {
value.obfuscateId()
@@ -43,6 +44,16 @@ data class QualifiedID(
fun toPlainID(): PlainId = PlainId(value)
+ /**
+ * This checks if the domain in either instances is blank. If it is, it will compare only the value.
+ * To be used when when of the instance do not have domain due to the API limitations.
+ */
+ fun equalsIgnoringBlankDomain(other: QualifiedID): Boolean {
+ if (domain.isBlank() || other.domain.isBlank()) {
+ return value == other.value
+ }
+ return this == other
+ }
}
const val VALUE_DOMAIN_SEPARATOR = '@'
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt
index 578ce4d1617..3cfd14e6525 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt
@@ -46,5 +46,11 @@ enum class LogoutReason {
/**
* Session Expired.
*/
- SESSION_EXPIRED;
+ SESSION_EXPIRED,
+
+ /**
+ * The migration to CC failed.
+ * This will trigger a cleanup of the local client data and prepare for a fresh start without losing data.
+ */
+ MIGRATION_TO_CC_FAILED
}
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt
index 1c77a460c34..3b1fe3cde5a 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt
@@ -34,7 +34,7 @@ data class AssetContent(
}
// We should not display Preview Assets (assets w/o valid encryption keys sent by Mac/Web clients) unless they include image metadata
- val shouldBeDisplayed = !isPreviewMessage || hasValidImageMetadata
+ val isAssetDataComplete = !isPreviewMessage || hasValidImageMetadata
sealed class AssetMetadata {
data class Image(val width: Int, val height: Int) : AssetMetadata()
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt
index d28cfa435f4..5ea75254462 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt
@@ -29,6 +29,8 @@ import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString
import com.wire.kalium.util.serialization.toJsonElement
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.time.Duration
@@ -120,6 +122,7 @@ sealed interface Message {
is MessageContent.FailedDecryption -> {
mutableMapOf(
typeKey to "failedDecryption",
+ "code" to "${content.errorCode}",
"size" to "${content.encodedData?.size}",
)
}
@@ -472,15 +475,20 @@ sealed interface Message {
}
}
+ @Serializable
data class ExpirationData(
- val expireAfter: Duration,
- val selfDeletionStatus: SelfDeletionStatus = SelfDeletionStatus.NotStarted
+ @SerialName("expire_after") val expireAfter: Duration,
+ @SerialName("self_deletion_status") val selfDeletionStatus: SelfDeletionStatus = SelfDeletionStatus.NotStarted
) {
+ @Serializable
sealed class SelfDeletionStatus {
+
+ @Serializable
data object NotStarted : SelfDeletionStatus()
- data class Started(val selfDeletionEndDate: Instant) : SelfDeletionStatus()
+ @Serializable
+ data class Started(@SerialName("self_deletion_end_date") val selfDeletionEndDate: Instant) : SelfDeletionStatus()
fun toLogMap(): Map = when (this) {
is NotStarted -> mutableMapOf(
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt
index f38a9b074cb..56103c2371f 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt
@@ -350,6 +350,7 @@ sealed interface MessageContent {
data class FailedDecryption(
val encodedData: ByteArray? = null,
+ val errorCode: Int? = null,
val isDecryptionResolved: Boolean,
val senderUserId: UserId,
val clientId: ClientId? = null
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt
index 9fcb19f0f22..419b9d502e8 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt
@@ -31,5 +31,6 @@ data class UserSummary(
val userType: UserType,
val isUserDeleted: Boolean,
val connectionStatus: ConnectionState,
- val availabilityStatus: UserAvailabilityStatus
+ val availabilityStatus: UserAvailabilityStatus,
+ val accentId: Int
)
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt
index 504fac6903c..bb0b8069452 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt
@@ -19,10 +19,13 @@
package com.wire.kalium.logic.data.message.mention
import com.wire.kalium.logic.data.user.UserId
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+@Serializable
data class MessageMention(
- val start: Int,
- val length: Int,
- val userId: UserId,
- val isSelfMention: Boolean
+ @SerialName("start") val start: Int,
+ @SerialName("length") val length: Int,
+ @SerialName("userId") val userId: UserId,
+ @SerialName("isSelfMention") val isSelfMention: Boolean
)
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/mls/MLSPublicKeys.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/mls/MLSPublicKeys.kt
new file mode 100644
index 00000000000..0475345d7fd
--- /dev/null
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/mls/MLSPublicKeys.kt
@@ -0,0 +1,22 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.mls
+
+data class MLSPublicKeys(
+ val removal: Map?
+)
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt
new file mode 100644
index 00000000000..622d28ad298
--- /dev/null
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt
@@ -0,0 +1,3 @@
+package com.wire.kalium.logic.data.user
+
+data class CreateUserTeam(val teamId: String, val teamName: String)
diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt
index 4abbcffcf71..31ae4050545 100644
--- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt
+++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt
@@ -145,6 +145,7 @@ data class OtherUserMinimized(
val name: String?,
val completePicture: UserAssetId?,
val userType: UserType,
+ val accentId: Int
)
data class OtherUser(
diff --git a/detekt/baseline.xml b/detekt/baseline.xml
index 05f36ee07dc..5ef15ecdce9 100644
--- a/detekt/baseline.xml
+++ b/detekt/baseline.xml
@@ -2,6 +2,7 @@
MatchingDeclarationName:Widgets.kt$CustomScrollRegion : Widget
+ UnusedPrivateProperty:build.gradle.kts$val commonMain by sourceSets.getting { dependencies { implementation(project(":network")) implementation(project(":cryptography")) implementation(project(":logic")) implementation(project(":util")) implementation(libs.cliKt) implementation(libs.ktor.utils) implementation(libs.coroutines.core) implementation(libs.ktxDateTime) implementation(libs.mordant) implementation(libs.ktxSerialization) implementation(libs.ktxIO) } }
AnnotationSpacing:HttpClientConnectionSpecsTest.kt$HttpClientConnectionSpecsTest$@Test
diff --git a/docs/notebooks/README.md b/docs/notebooks/README.md
new file mode 100644
index 00000000000..e23e2f87603
--- /dev/null
+++ b/docs/notebooks/README.md
@@ -0,0 +1,3 @@
+A place to store [Jupyter Notebook](https://jupyter.org/) files of ongoing studies.
+
+You can use Kotlin and run it directly in the IDE, thanks to [Kotlin Notebook](https://kotlinlang.org/docs/kotlin-notebook-overview.html).
diff --git a/docs/notebooks/event-performance-investigation/PerformanceImprovements.ipynb b/docs/notebooks/event-performance-investigation/PerformanceImprovements.ipynb
new file mode 100644
index 00000000000..7bc2901111d
--- /dev/null
+++ b/docs/notebooks/event-performance-investigation/PerformanceImprovements.ipynb
@@ -0,0 +1,5158 @@
+{
+ "cells": [
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "# Event Processing Analysis\n",
+ "\n",
+ "### What's the goal?\n",
+ "Understand where Kalium is spending more time during event processing, in order to find possible performance improvements.\n",
+ "\n",
+ "\n",
+ "### Current context\n",
+ "- We've added [logs that measure how much time we're taking to process individual events](https://github.com/wireapp/kalium/pull/2827).\n",
+ "- The average time to process events oscilates every day between 368ms all the way up to 1280ms;\n",
+ "- 97,3% of the time spent processing events was processing end-to-end encrypted messages;\n",
+ "- 51% of the time spent processing messages was in order to process Text messages, even though they only account for 24,04% of the messages;\n",
+ "- `LastRead` is another interesting one: 18,57% of the count, but takes 24,3% of the time\n",
+ "\n",
+ "## What even is measured within \"Event Processing\"?\n",
+ "We measure all the time taken from the beginning of `EventReceiver.onEvent` until the event is handled completely.\n",
+ "For some Event Types, this can be super quick, like handling in-conversation Typing indication. Typing indication consists of just emitting to an in-memory flow and the event is fully processed.\n",
+ "For some events, this can be tricky. All End-To-End messages require parsing the encrypted blob, parsing the Protobuf, and then \"actually\" handling the different message types. Different message types often need completely different logic, so they can be as diverse as different event types.\n",
+ "\n",
+ "## Starting plan of action\n",
+ "Collect data locally, by adding more logs and generating events.\n",
+ "\n",
+ "My first fail was using an emulator. Running the debug of the Android app on the emulator resulted in nothing but weird looks. DataDog was giving hundreds of milliseconds as the average for event processing, while my emulator was taking about 20~30ms. \n",
+ "\n",
+ "How could that be? Running on an actual device – with the screen locked – made it right. An emulator has almost no apps installed competing for resources with the app being tested, and isn't prone to battery saving measures like CPU speed reduction, etc.\n",
+ " \n",
+ "## Lesson #1\n",
+ "We can focus on \"background performance\": receive a push notification, sync, decrypt, show notifications.\n",
+ "Or on \"foreground performance\": open the app, sync, decrypt, show notifications and unread indicators.\n",
+ "\n",
+ "The main difference is how the app might be subject to battery saving measures taken by the OS of a real device, and the time to perform the same actions might be completely different between the two scenarios.\n",
+ "Proposed action:\n",
+ "- Investigate further if we can add battery, back/fore-ground information, etc. to logs; This way we can track performance indication separately.\n",
+ "\n",
+ "## Keep digging \n",
+ "\n",
+ "Considering how there are multiple steps to \"event processing\", I've added logs to gather more information about these steps and their \"substeps\".\n",
+ "Also, considering 97,3% of the time is spent during Messages processing, I focused on these.\n",
+ "\n",
+ "Time spent during message events processing can be seen defined below in `TimeTaken`. \n",
+ " "
+ ]
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "Just adding some dependencies for manipulating data and Kotlin's datetime"
+ },
+ {
+ "metadata": {
+ "collapsed": true,
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:22.641327700Z",
+ "start_time": "2024-09-02T08:30:19.253500300Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "%use kandy\n",
+ "%use dataframe"
+ ],
+ "outputs": [],
+ "execution_count": 13
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:22.690290300Z",
+ "start_time": "2024-09-02T08:30:22.645582300Z"
+ }
+ },
+ "cell_type": "code",
+ "source": "@file:DependsOn(\"org.jetbrains.kotlinx:kotlinx-datetime:0.6.1\")",
+ "outputs": [],
+ "execution_count": 14
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "Defining a way of representing event processing measurements and how to import data from the `logs`:"
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:23.007982300Z",
+ "start_time": "2024-09-02T08:30:22.695901100Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "\n",
+ "import kotlin.time.Duration\n",
+ "import kotlin.time.Duration.Companion.milliseconds\n",
+ "\n",
+ "data class ProteusDecryption(\n",
+ " val checkingSession: Duration,\n",
+ " val decrypting: Duration,\n",
+ " val savingSession: Duration,\n",
+ ") {\n",
+ " val total: Duration = checkingSession + decrypting + savingSession\n",
+ "}\n",
+ "\n",
+ "data class ProcessingMessage(\n",
+ " /**\n",
+ " * Dealing with possibility of LegalHold\n",
+ " */\n",
+ " val checkingLegalhold: Duration,\n",
+ " /**\n",
+ " * Actually handling the content of the message,\n",
+ " * if LastRead, marking as read in the DB,\n",
+ " * if Text, inserting into the DB, emitting notifications,\n",
+ " * ...\n",
+ " */\n",
+ " val handlingMessageContent: Duration,\n",
+ " /**\n",
+ " * Other actions we take after the message is handled, like enqueueing delivery receipt\n",
+ " * and enqueueing message deletion if the message is SelfDeleting\n",
+ " */\n",
+ " val postHandling: Duration\n",
+ ") {\n",
+ " val total: Duration = checkingLegalhold + handlingMessageContent + postHandling\n",
+ "}\n",
+ "\n",
+ "data class TimeTaken(\n",
+ " val messageType: String,\n",
+ " val total: Duration,\n",
+ " val proteus: ProteusDecryption,\n",
+ " val processingMessage: ProcessingMessage\n",
+ ") {\n",
+ " /**\n",
+ " * Unknown time can be things like parsing the Protobuf, converting the blob from Base64, etc.\n",
+ " * In general: it shouldn't be slow, and it's less likely we can speed it up.\n",
+ " */\n",
+ " val unknownTime = total - proteus.total - processingMessage.total\n",
+ "}\n",
+ "\n",
+ "fun getDataFromLogs(logFileName: String): List {\n",
+ " val file = File(\"logs/$logFileName\")\n",
+ " val groups = file.readText().split(\"\"\"^$\"\"\".toRegex(RegexOption.MULTILINE))\n",
+ "\n",
+ " val allData = groups.map {\n",
+ " it.trim().split(\"\\n\").filter { it.isNotBlank() }\n",
+ " }.filter {\n",
+ " it.isNotEmpty()\n",
+ " }.map { lines ->\n",
+ " val logLine = lines[0]\n",
+ " val measurements = logLine.split(\";\").map { it.split(\" \").last() }\n",
+ " val proteusDuration = ProteusDecryption(\n",
+ " Duration.parse(measurements[0].trim()),\n",
+ " Duration.parse(measurements[1].trim()),\n",
+ " Duration.parse(measurements[2].trim()),\n",
+ " )\n",
+ "\n",
+ " val processingLine = lines[1]\n",
+ " val processingMeasurements = processingLine.split(\";\").map { it.split(\" \").last() }\n",
+ " val processingDuration = ProcessingMessage(\n",
+ " Duration.parse(processingMeasurements[0].trim()),\n",
+ " Duration.parse(processingMeasurements[1].trim()),\n",
+ " Duration.parse(processingMeasurements[2].trim()),\n",
+ " )\n",
+ " val mainInfo = lines[2].split(\" \")\n",
+ " val timeTaken = mainInfo[0].toLong().milliseconds\n",
+ " val messageType = mainInfo[1]\n",
+ " TimeTaken(messageType, timeTaken, proteusDuration, processingDuration)\n",
+ " }\n",
+ " return allData\n",
+ "}"
+ ],
+ "outputs": [],
+ "execution_count": 15
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "Ok, so let's do a simple analysis of how much time is spent during Proteus, Message handling and other."
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:23.933356Z",
+ "start_time": "2024-09-02T08:30:23.018790400Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "@file:DependsOn(\"org.jetbrains.kotlinx:kotlinx-datetime:0.6.1\")\n",
+ "\n",
+ "val initialData = getDataFromLogs(\"InitialData.txt\")\n",
+ "\n",
+ "val pairs =\n",
+ " initialData.map { it.proteus.total.inWholeMicroseconds to \"Proteus\" } +\n",
+ " initialData.map { it.processingMessage.total.inWholeMicroseconds to \"Processing\" } +\n",
+ " initialData.map { it.unknownTime.inWholeMicroseconds to \"Other\" }\n",
+ "\n",
+ "val value by column(pairs.map { it.first })\n",
+ "val stepName by column(pairs.map { it.second })\n",
+ "val dataSet = dataFrameOf(value, stepName)\n",
+ "dataSet.groupBy(stepName).aggregate {\n",
+ " sum(value) into \"total\"\n",
+ "}.plot {\n",
+ " pie {\n",
+ " slice(\"total\")\n",
+ " fillColor(\"stepName\")\n",
+ " }\n",
+ " layout {\n",
+ " title = \"Time spent unpacking and processing message events\"\n",
+ " style(Style.Void)\n",
+ " }\n",
+ "}"
+ ],
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "application/plot+json": {
+ "output_type": "lets_plot_spec",
+ "output": {
+ "ggtitle": {
+ "text": "Time spent unpacking and processing message events"
+ },
+ "mapping": {},
+ "data": {
+ "total": [
+ 3753588.0,
+ 4459976.0,
+ 804380.0
+ ],
+ "stepName": [
+ "Proteus",
+ "Processing",
+ "Other"
+ ]
+ },
+ "kind": "plot",
+ "scales": [
+ {
+ "aesthetic": "fill",
+ "discrete": true
+ }
+ ],
+ "layers": [
+ {
+ "mapping": {
+ "slice": "total",
+ "fill": "stepName"
+ },
+ "stat": "identity",
+ "sampling": "none",
+ "position": "identity",
+ "geom": "pie"
+ }
+ ],
+ "theme": {
+ "name": "classic",
+ "axis": {
+ "blank": true
+ },
+ "line": {
+ "blank": true
+ },
+ "axis_ontop": false,
+ "axis_ontop_y": false,
+ "axis_ontop_x": false
+ }
+ },
+ "apply_color_scheme": true,
+ "swing_enabled": true
+ }
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 16
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "While Processing clearly is the slowest part, the Proteus part is still interesting. Because it affects _all_ message types.\n",
+ "Processing can be very different between messages and requires an investigation for _each_ message type to find bottlenecks and improvement opportunities.\n",
+ "\n",
+ "With that in mind, let's dive a bit deeper into Proteus. Let's split it into smaller chunks and see what we can improve there."
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:24.568994Z",
+ "start_time": "2024-09-02T08:30:23.971615300Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "val initialProteusMeasurements = initialData.map { it.proteus }\n",
+ "val checkingSession by column(initialProteusMeasurements.map { it.checkingSession.inWholeMilliseconds })\n",
+ "val decrypting by column(initialProteusMeasurements.map { it.decrypting.inWholeMilliseconds })\n",
+ "val savingSession by column(initialProteusMeasurements.map { it.savingSession.inWholeMilliseconds })\n",
+ "val eventNumber by column(buildList { repeat(initialProteusMeasurements.size) { i -> add(i + 1)} })\n",
+ "val detailedFrame = dataFrameOf(eventNumber, checkingSession, decrypting, savingSession)\n",
+ " .gather(checkingSession, decrypting, savingSession)\n",
+ " .into(\"measurements\", \"timeSpent\")\n",
+ "\n",
+ "detailedFrame.plot {\n",
+ " layout.title = \"Time taken in each phase of Proteus Unpacking, for different events\"\n",
+ " points {\n",
+ " x(\"measurements\")\n",
+ " y(\"timeSpent\")\n",
+ " color(\"measurements\")\n",
+ " position = Position.jitter()\n",
+ " }\n",
+ "}\n"
+ ],
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "application/plot+json": {
+ "output_type": "lets_plot_spec",
+ "output": {
+ "ggtitle": {
+ "text": "Time taken in each phase of Proteus Unpacking, for different events"
+ },
+ "mapping": {},
+ "data": {
+ "timeSpent": [
+ 8.0,
+ 63.0,
+ 30.0,
+ 10.0,
+ 36.0,
+ 21.0,
+ 19.0,
+ 25.0,
+ 49.0,
+ 10.0,
+ 30.0,
+ 50.0,
+ 40.0,
+ 77.0,
+ 21.0,
+ 5.0,
+ 20.0,
+ 46.0,
+ 9.0,
+ 20.0,
+ 20.0,
+ 37.0,
+ 117.0,
+ 34.0,
+ 129.0,
+ 61.0,
+ 12.0,
+ 9.0,
+ 69.0,
+ 19.0,
+ 34.0,
+ 106.0,
+ 57.0,
+ 16.0,
+ 10.0,
+ 76.0,
+ 7.0,
+ 85.0,
+ 56.0,
+ 11.0,
+ 60.0,
+ 13.0,
+ 6.0,
+ 13.0,
+ 85.0,
+ 13.0,
+ 28.0,
+ 30.0,
+ 6.0,
+ 12.0,
+ 14.0,
+ 18.0,
+ 56.0,
+ 10.0,
+ 5.0,
+ 22.0,
+ 43.0,
+ 22.0,
+ 15.0,
+ 18.0,
+ 7.0,
+ 11.0,
+ 25.0,
+ 37.0,
+ 50.0,
+ 31.0,
+ 6.0,
+ 10.0,
+ 17.0,
+ 7.0,
+ 77.0,
+ 60.0,
+ 6.0,
+ 76.0,
+ 12.0,
+ 9.0,
+ 17.0,
+ 10.0,
+ 7.0,
+ 10.0,
+ 8.0,
+ 14.0,
+ 28.0,
+ 92.0,
+ 18.0,
+ 14.0,
+ 35.0,
+ 9.0,
+ 10.0,
+ 32.0,
+ 58.0,
+ 15.0,
+ 46.0,
+ 48.0,
+ 82.0,
+ 15.0,
+ 4.0,
+ 34.0,
+ 17.0,
+ 27.0,
+ 28.0,
+ 18.0,
+ 27.0,
+ 66.0,
+ 30.0,
+ 48.0,
+ 90.0,
+ 57.0,
+ 44.0,
+ 53.0,
+ 49.0,
+ 12.0,
+ 35.0,
+ 9.0
+ ],
+ "measurements": [
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession",
+ "checkingSession",
+ "decrypting",
+ "savingSession"
+ ]
+ },
+ "kind": "plot",
+ "scales": [
+ {
+ "aesthetic": "x",
+ "discrete": true
+ },
+ {
+ "aesthetic": "y",
+ "limits": [
+ null,
+ null
+ ]
+ },
+ {
+ "aesthetic": "color",
+ "discrete": true
+ }
+ ],
+ "layers": [
+ {
+ "mapping": {
+ "x": "measurements",
+ "y": "timeSpent",
+ "color": "measurements"
+ },
+ "stat": "identity",
+ "sampling": "none",
+ "position": "jitter",
+ "geom": "point"
+ }
+ ]
+ },
+ "apply_color_scheme": true,
+ "swing_enabled": true
+ }
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 17
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "While the colours might cause seizure, it goes to show how random it is. In some times saving session is slow. In some times checking session is slow. In some times decrypting is slow.\n",
+ "\n",
+ "One thing can definitely be seen from this, is that decrypting is the slowest part.\n",
+ "\n",
+ "Unfortunately, decrypting is something we can't solve on our end. It's embedded within CoreCrypto / CryptoBox, and we are unable to change it.\n",
+ "\n",
+ "Here's a simpler visualization:"
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:24.856529300Z",
+ "start_time": "2024-09-02T08:30:24.608176900Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "val steps by columnOf(\"Checking Session\", \"Decrypting\", \"Saving Session\")\n",
+ "val values by columnOf(checkingSession.sum(), decrypting.sum(), savingSession.sum())\n",
+ "\n",
+ "val dataFrame = dataFrameOf(steps, values)\n",
+ "dataFrame.plot {\n",
+ " pie {\n",
+ " slice(values)\n",
+ " fillColor(steps)\n",
+ " }\n",
+ " layout {\n",
+ " title = \"Time share in different Proteus phases\"\n",
+ " style(Style.Void)\n",
+ " }\n",
+ "}"
+ ],
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "application/plot+json": {
+ "output_type": "lets_plot_spec",
+ "output": {
+ "ggtitle": {
+ "text": "Time share in different Proteus phases"
+ },
+ "mapping": {},
+ "data": {
+ "values": [
+ 802.0,
+ 1631.0,
+ 1267.0
+ ],
+ "steps": [
+ "Checking Session",
+ "Decrypting",
+ "Saving Session"
+ ]
+ },
+ "kind": "plot",
+ "scales": [
+ {
+ "aesthetic": "fill",
+ "discrete": true
+ }
+ ],
+ "layers": [
+ {
+ "mapping": {
+ "slice": "values",
+ "fill": "steps"
+ },
+ "stat": "identity",
+ "sampling": "none",
+ "position": "identity",
+ "geom": "pie"
+ }
+ ],
+ "theme": {
+ "name": "classic",
+ "axis": {
+ "blank": true
+ },
+ "line": {
+ "blank": true
+ },
+ "axis_ontop": false,
+ "axis_ontop_y": false,
+ "axis_ontop_x": false
+ }
+ },
+ "apply_color_scheme": true,
+ "swing_enabled": true
+ }
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 18
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "Clearly decrypting is the biggest offender. And as mentioned before, we can't do much about it.\n",
+ "\n",
+ "However, we might have quick wins and reduce `Saving Session` and `Checking Session`, which account for almost 50% of the total Proteus time."
+ ]
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "## Idea #1\n",
+ "Remove `Saving Session` completely. It's unnecessary as CoreCrypto will perform session saving in every `decrypt`. We're doing that unnecessarily.\n",
+ "\n",
+ "## Idea #2\n",
+ "Cache existing sessions in-memory. Instead of using IO and relying solely on CoreCrypto to know if sessions exist or not, we can store known sessions in memory.\n",
+ "If sessions are deleted, we can remove from the memory cache. \n",
+ "\n",
+ "## Results\n",
+ "After doing the first idea, I collected data and stored it in `NotSavingSession.txt`. After that, I also did the second idea and collected data, bringing to the joined improvements."
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:25.503196600Z",
+ "start_time": "2024-09-02T08:30:24.972036600Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "val withoutSessionSavingData = getDataFromLogs(\"NotSavingSession.txt\")\n",
+ "val withSessionCacheData = getDataFromLogs(\"WithExistingSessionCache.txt\")\n",
+ "\n",
+ "val initialTime = initialData.map { it.total.inWholeMilliseconds to \"Initial\" }\n",
+ "val withoutSessionSaving =\n",
+ " withoutSessionSavingData.map { it.total.inWholeMilliseconds to \"NoSessionSaving\" }\n",
+ "val withCache = withSessionCacheData.map { it.total.inWholeMilliseconds to \"WithSessionCache\" }\n",
+ "val allData = (initialTime + withoutSessionSaving + withCache)\n",
+ "val timeTaken by column(allData.map { it.first })\n",
+ "val approach by column(allData.map { it.second })\n",
+ "\n",
+ "val averages = listOf(initialTime.map { it.first }.average(), withoutSessionSaving.map { it.first }.average(), withCache.map { it.first }.average())\n",
+ "val dataFrame = dataFrameOf(timeTaken, approach)\n",
+ "\n",
+ "dataFrame.plot {\n",
+ " layout.title = \"Average time taken to fully process events across different approaches\"\n",
+ " points {\n",
+ " y(timeTaken)\n",
+ " x(approach)\n",
+ " color(approach)\n",
+ " position = Position.jitter()\n",
+ " }\n",
+ " line {\n",
+ " y(averages) {\n",
+ " axis.name = \"Time Taken (ms)\"\n",
+ " }\n",
+ " x(approach.distinct()) {\n",
+ " axis.name = \"Approach\"\n",
+ " }\n",
+ " color = Color.PURPLE\n",
+ " this.tooltips(true)\n",
+ " }\n",
+ "}"
+ ],
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "application/plot+json": {
+ "output_type": "lets_plot_spec",
+ "output": {
+ "ggtitle": {
+ "text": "Average time taken to fully process events across different approaches"
+ },
+ "mapping": {},
+ "data": {
+ "timeTaken": [
+ 349.0,
+ 150.0,
+ 240.0,
+ 347.0,
+ 268.0,
+ 182.0,
+ 112.0,
+ 363.0,
+ 481.0,
+ 299.0,
+ 400.0,
+ 242.0,
+ 322.0,
+ 150.0,
+ 183.0,
+ 161.0,
+ 104.0,
+ 178.0,
+ 159.0,
+ 127.0,
+ 174.0,
+ 306.0,
+ 161.0,
+ 233.0,
+ 218.0,
+ 86.0,
+ 122.0,
+ 313.0,
+ 261.0,
+ 201.0,
+ 286.0,
+ 275.0,
+ 164.0,
+ 203.0,
+ 325.0,
+ 378.0,
+ 337.0,
+ 158.0,
+ 412.0,
+ 267.0,
+ 140.0,
+ 292.0,
+ 444.0,
+ 127.0,
+ 226.0,
+ 231.0,
+ 327.0,
+ 147.0,
+ 205.0,
+ 330.0,
+ 286.0,
+ 314.0,
+ 172.0,
+ 198.0,
+ 360.0,
+ 124.0,
+ 232.0,
+ 151.0,
+ 181.0,
+ 112.0,
+ 162.0,
+ 80.0,
+ 72.0,
+ 68.0,
+ 122.0,
+ 191.0,
+ 89.0,
+ 104.0,
+ 177.0,
+ 72.0,
+ 72.0,
+ 109.0,
+ 240.0,
+ 117.0,
+ 373.0,
+ 201.0,
+ 106.0,
+ 65.0,
+ 128.0,
+ 191.0,
+ 147.0,
+ 143.0,
+ 112.0,
+ 133.0,
+ 168.0,
+ 251.0,
+ 180.0,
+ 343.0,
+ 200.0,
+ 171.0,
+ 164.0,
+ 104.0,
+ 102.0,
+ 77.0,
+ 103.0,
+ 191.0,
+ 129.0,
+ 204.0,
+ 117.0,
+ 73.0,
+ 198.0,
+ 165.0,
+ 190.0,
+ 78.0,
+ 172.0,
+ 124.0,
+ 172.0,
+ 155.0,
+ 74.0,
+ 54.0,
+ 259.0,
+ 103.0,
+ 89.0,
+ 168.0,
+ 58.0,
+ 135.0,
+ 92.0,
+ 136.0,
+ 97.0,
+ 145.0,
+ 145.0,
+ 151.0,
+ 263.0,
+ 204.0,
+ 160.0,
+ 221.0,
+ 116.0,
+ 92.0,
+ 138.0
+ ],
+ "approach": [
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "Initial",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "NoSessionSaving",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache",
+ "WithSessionCache"
+ ]
+ },
+ "kind": "plot",
+ "scales": [
+ {
+ "aesthetic": "y",
+ "limits": [
+ null,
+ null
+ ]
+ },
+ {
+ "aesthetic": "x",
+ "discrete": true
+ },
+ {
+ "aesthetic": "color",
+ "discrete": true
+ },
+ {
+ "aesthetic": "y",
+ "name": "Time Taken (ms)",
+ "limits": [
+ null,
+ null
+ ]
+ },
+ {
+ "aesthetic": "x",
+ "discrete": true,
+ "name": "Approach"
+ }
+ ],
+ "layers": [
+ {
+ "mapping": {
+ "y": "timeTaken",
+ "x": "approach",
+ "color": "approach"
+ },
+ "stat": "identity",
+ "sampling": "none",
+ "position": "jitter",
+ "geom": "point"
+ },
+ {
+ "mapping": {
+ "y": "y",
+ "x": "approach"
+ },
+ "stat": "identity",
+ "data": {
+ "y": [
+ 237.31578947368422,
+ 195.71794871794873,
+ 147.1153846153846
+ ],
+ "approach": [
+ "Initial",
+ "NoSessionSaving",
+ "WithSessionCache"
+ ]
+ },
+ "color": "#9a60b4",
+ "sampling": "none",
+ "position": "identity",
+ "geom": "line",
+ "tooltips": {}
+ }
+ ]
+ },
+ "apply_color_scheme": true,
+ "swing_enabled": true
+ }
+ },
+ "execution_count": 19,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 19
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "## First win\n",
+ "Clearly we're up to something. Processing events is clearly faster with the changes in Proteus. The average time went from 247ms, to 196ms without saving session, and then down to 147ms without saving session + caching existing sessions.\n",
+ "\n",
+ "## But we're not over yet\n",
+ "Maybe we can also analyse the different parts of `ProcessingMessage` and identify what is taking more time? For that, we can join all the logs taken so far and ignore the Proteus part."
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:25.811756500Z",
+ "start_time": "2024-09-02T08:30:25.533313800Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "val allMeasurements = initialData + withoutSessionSavingData + withSessionCacheData\n",
+ "val allMessageProcessing = allMeasurements.map { \n",
+ " it.processingMessage\n",
+ "}\n",
+ "val checkingLegalhold by column(allMessageProcessing.map { it.checkingLegalhold.inWholeMilliseconds })\n",
+ "val handlingContent by column(allMessageProcessing.map { it.handlingMessageContent.inWholeMilliseconds })\n",
+ "val postHandling by column(allMessageProcessing.map { it.postHandling.inWholeMilliseconds })\n",
+ "val eventNumber by column(buildList { repeat(allMessageProcessing.size) { i -> add(i + 1)} })\n",
+ "val detailedFrame = dataFrameOf(eventNumber, checkingLegalhold, handlingContent, postHandling)\n",
+ " .gather(checkingLegalhold, handlingContent, postHandling)\n",
+ " .into(\"measurements\", \"timeSpent\")\n",
+ "\n",
+ "detailedFrame.plot { \n",
+ " points { \n",
+ " x(\"measurements\")\n",
+ " y(\"timeSpent\")\n",
+ " position = Position.jitter()\n",
+ " color(\"measurements\")\n",
+ " }\n",
+ "}\n"
+ ],
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "application/plot+json": {
+ "output_type": "lets_plot_spec",
+ "output": {
+ "mapping": {},
+ "data": {
+ "timeSpent": [
+ 5.0,
+ 130.0,
+ 0.0,
+ 23.0,
+ 52.0,
+ 0.0,
+ 12.0,
+ 100.0,
+ 0.0,
+ 86.0,
+ 141.0,
+ 0.0,
+ 9.0,
+ 106.0,
+ 0.0,
+ 3.0,
+ 99.0,
+ 0.0,
+ 4.0,
+ 41.0,
+ 0.0,
+ 11.0,
+ 142.0,
+ 0.0,
+ 131.0,
+ 124.0,
+ 0.0,
+ 12.0,
+ 144.0,
+ 8.0,
+ 6.0,
+ 179.0,
+ 0.0,
+ 17.0,
+ 106.0,
+ 0.0,
+ 19.0,
+ 112.0,
+ 0.0,
+ 8.0,
+ 50.0,
+ 0.0,
+ 21.0,
+ 52.0,
+ 0.0,
+ 4.0,
+ 62.0,
+ 0.0,
+ 5.0,
+ 40.0,
+ 0.0,
+ 5.0,
+ 79.0,
+ 0.0,
+ 3.0,
+ 78.0,
+ 0.0,
+ 4.0,
+ 49.0,
+ 0.0,
+ 4.0,
+ 119.0,
+ 0.0,
+ 18.0,
+ 139.0,
+ 0.0,
+ 12.0,
+ 84.0,
+ 0.0,
+ 26.0,
+ 55.0,
+ 0.0,
+ 7.0,
+ 104.0,
+ 0.0,
+ 5.0,
+ 30.0,
+ 0.0,
+ 3.0,
+ 87.0,
+ 0.0,
+ 4.0,
+ 106.0,
+ 0.0,
+ 28.0,
+ 144.0,
+ 0.0,
+ 4.0,
+ 105.0,
+ 0.0,
+ 39.0,
+ 101.0,
+ 0.0,
+ 6.0,
+ 72.0,
+ 0.0,
+ 14.0,
+ 86.0,
+ 0.0,
+ 24.0,
+ 92.0,
+ 0.0,
+ 3.0,
+ 191.0,
+ 0.0,
+ 13.0,
+ 161.0,
+ 0.0,
+ 33.0,
+ 122.0,
+ 0.0,
+ 16.0,
+ 79.0,
+ 0.0,
+ 0.0,
+ 97.0,
+ 0.0,
+ 10.0,
+ 181.0,
+ 0.0,
+ 9.0,
+ 75.0,
+ 0.0,
+ 5.0,
+ 138.0,
+ 0.0,
+ 4.0,
+ 346.0,
+ 0.0,
+ 14.0,
+ 43.0,
+ 0.0,
+ 5.0,
+ 104.0,
+ 0.0,
+ 33.0,
+ 58.0,
+ 0.0,
+ 4.0,
+ 89.0,
+ 0.0,
+ 9.0,
+ 87.0,
+ 0.0,
+ 4.0,
+ 82.0,
+ 0.0,
+ 10.0,
+ 139.0,
+ 0.0,
+ 6.0,
+ 125.0,
+ 0.0,
+ 28.0,
+ 121.0,
+ 0.0,
+ 9.0,
+ 101.0,
+ 0.0,
+ 4.0,
+ 111.0,
+ 0.0,
+ 5.0,
+ 210.0,
+ 0.0,
+ 7.0,
+ 26.0,
+ 0.0,
+ 43.0,
+ 84.0,
+ 0.0,
+ 5.0,
+ 65.0,
+ 0.0,
+ 6.0,
+ 57.0,
+ 0.0,
+ 5.0,
+ 37.0,
+ 0.0,
+ 4.0,
+ 61.0,
+ 0.0,
+ 4.0,
+ 20.0,
+ 0.0,
+ 9.0,
+ 16.0,
+ 0.0,
+ 6.0,
+ 14.0,
+ 0.0,
+ 10.0,
+ 25.0,
+ 0.0,
+ 7.0,
+ 45.0,
+ 0.0,
+ 4.0,
+ 29.0,
+ 0.0,
+ 4.0,
+ 16.0,
+ 0.0,
+ 40.0,
+ 24.0,
+ 0.0,
+ 6.0,
+ 21.0,
+ 0.0,
+ 19.0,
+ 11.0,
+ 0.0,
+ 4.0,
+ 58.0,
+ 0.0,
+ 4.0,
+ 120.0,
+ 0.0,
+ 4.0,
+ 52.0,
+ 0.0,
+ 79.0,
+ 47.0,
+ 0.0,
+ 10.0,
+ 106.0,
+ 0.0,
+ 25.0,
+ 16.0,
+ 0.0,
+ 0.0,
+ 30.0,
+ 0.0,
+ 8.0,
+ 87.0,
+ 0.0,
+ 43.0,
+ 84.0,
+ 0.0,
+ 22.0,
+ 82.0,
+ 0.0,
+ 18.0,
+ 39.0,
+ 0.0,
+ 12.0,
+ 59.0,
+ 0.0,
+ 4.0,
+ 43.0,
+ 0.0,
+ 4.0,
+ 74.0,
+ 0.0,
+ 17.0,
+ 160.0,
+ 0.0,
+ 29.0,
+ 32.0,
+ 0.0,
+ 8.0,
+ 110.0,
+ 0.0,
+ 26.0,
+ 100.0,
+ 0.0,
+ 32.0,
+ 77.0,
+ 0.0,
+ 13.0,
+ 59.0,
+ 0.0,
+ 22.0,
+ 54.0,
+ 0.0,
+ 7.0,
+ 56.0,
+ 0.0,
+ 4.0,
+ 30.0,
+ 0.0,
+ 13.0,
+ 23.0,
+ 0.0,
+ 5.0,
+ 63.0,
+ 0.0,
+ 7.0,
+ 45.0,
+ 0.0,
+ 4.0,
+ 82.0,
+ 0.0,
+ 5.0,
+ 41.0,
+ 0.0,
+ 5.0,
+ 23.0,
+ 0.0,
+ 5.0,
+ 84.0,
+ 0.0,
+ 6.0,
+ 44.0,
+ 0.0,
+ 23.0,
+ 73.0,
+ 0.0,
+ 15.0,
+ 19.0,
+ 0.0,
+ 7.0,
+ 95.0,
+ 0.0,
+ 30.0,
+ 31.0,
+ 0.0,
+ 19.0,
+ 70.0,
+ 0.0,
+ 9.0,
+ 19.0,
+ 0.0,
+ 4.0,
+ 18.0,
+ 0.0,
+ 4.0,
+ 30.0,
+ 0.0,
+ 13.0,
+ 114.0,
+ 0.0,
+ 28.0,
+ 16.0,
+ 0.0,
+ 4.0,
+ 14.0,
+ 0.0,
+ 36.0,
+ 41.0,
+ 0.0,
+ 5.0,
+ 32.0,
+ 0.0,
+ 4.0,
+ 38.0,
+ 0.0,
+ 4.0,
+ 47.0,
+ 0.0,
+ 4.0,
+ 26.0,
+ 0.0,
+ 14.0,
+ 46.0,
+ 3.0,
+ 5.0,
+ 76.0,
+ 0.0,
+ 19.0,
+ 76.0,
+ 0.0,
+ 5.0,
+ 56.0,
+ 0.0,
+ 11.0,
+ 99.0,
+ 0.0,
+ 13.0,
+ 78.0,
+ 0.0,
+ 31.0,
+ 24.0,
+ 0.0,
+ 50.0,
+ 69.0,
+ 0.0,
+ 5.0,
+ 11.0,
+ 0.0,
+ 9.0,
+ 45.0,
+ 0.0,
+ 6.0,
+ 83.0,
+ 0.0
+ ],
+ "measurements": [
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling",
+ "checkingLegalhold",
+ "handlingContent",
+ "postHandling"
+ ]
+ },
+ "kind": "plot",
+ "scales": [
+ {
+ "aesthetic": "x",
+ "discrete": true
+ },
+ {
+ "aesthetic": "y",
+ "limits": [
+ null,
+ null
+ ]
+ },
+ {
+ "aesthetic": "color",
+ "discrete": true
+ }
+ ],
+ "layers": [
+ {
+ "mapping": {
+ "x": "measurements",
+ "y": "timeSpent",
+ "color": "measurements"
+ },
+ "stat": "identity",
+ "sampling": "none",
+ "position": "jitter",
+ "geom": "point"
+ }
+ ]
+ },
+ "apply_color_scheme": true,
+ "swing_enabled": true
+ }
+ },
+ "execution_count": 20,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 20
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "## Bad news\n",
+ "\n",
+ "While checking legalhold is definitely a bit more random (probably due to IO operations), handling each different content is definitely the biggest offender.\n",
+ "Here's a comparison across all different steps using the most improved data."
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-09-02T08:30:26.254403300Z",
+ "start_time": "2024-09-02T08:30:25.873610800Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "import org.jetbrains.kotlinx.kandy.ir.feature.FeatureName\n",
+ "\n",
+ "val pairs =\n",
+ " withSessionCacheData.map { it.proteus.checkingSession.inWholeMicroseconds to \"Checking Session\" } +\n",
+ " withSessionCacheData.map { it.proteus.decrypting.inWholeMicroseconds to \"Decrypting\" } +\n",
+ " withSessionCacheData.map { it.proteus.savingSession.inWholeMicroseconds to \"Saving Session\" } +\n",
+ " withSessionCacheData.map { it.processingMessage.checkingLegalhold.inWholeMicroseconds to \"Checking Legalhold\" } +\n",
+ " withSessionCacheData.map { it.processingMessage.handlingMessageContent.inWholeMicroseconds to \"Handling Content\" } +\n",
+ " withSessionCacheData.map { it.processingMessage.postHandling.inWholeMicroseconds to \"Post Handling\" } +\n",
+ " withSessionCacheData.map { it.unknownTime.inWholeMicroseconds to \"Other\" }\n",
+ "\n",
+ "val value by column(pairs.map { it.first })\n",
+ "val stepName by column(pairs.map { it.second })\n",
+ "val dataSet = dataFrameOf(value, stepName)\n",
+ "dataSet.groupBy(stepName).aggregate {\n",
+ " sum(value) into \"total\"\n",
+ "}.plot {\n",
+ " pie {\n",
+ " slice(\"total\")\n",
+ " fillColor(\"stepName\")\n",
+ " }\n",
+ " layout {\n",
+ " title = \"Time spent unpacking and processing message events after improvements\"\n",
+ " style(Style.Void)\n",
+ " }\n",
+ "}"
+ ],
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "application/plot+json": {
+ "output_type": "lets_plot_spec",
+ "output": {
+ "ggtitle": {
+ "text": "Time spent unpacking and processing message events after improvements"
+ },
+ "mapping": {},
+ "data": {
+ "total": [
+ 6634.0,
+ 2872468.0,
+ 232.0,
+ 721930.0,
+ 2952363.0,
+ 6072.0,
+ 1090122.0
+ ],
+ "stepName": [
+ "Checking Session",
+ "Decrypting",
+ "Saving Session",
+ "Checking Legalhold",
+ "Handling Content",
+ "Post Handling",
+ "Other"
+ ]
+ },
+ "kind": "plot",
+ "scales": [
+ {
+ "aesthetic": "fill",
+ "discrete": true
+ }
+ ],
+ "layers": [
+ {
+ "mapping": {
+ "slice": "total",
+ "fill": "stepName"
+ },
+ "stat": "identity",
+ "sampling": "none",
+ "position": "identity",
+ "geom": "pie"
+ }
+ ],
+ "theme": {
+ "name": "classic",
+ "axis": {
+ "blank": true
+ },
+ "line": {
+ "blank": true
+ },
+ "axis_ontop": false,
+ "axis_ontop_y": false,
+ "axis_ontop_x": false
+ }
+ },
+ "apply_color_scheme": true,
+ "swing_enabled": true
+ }
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "execution_count": 21
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": [
+ "## What's next?\n",
+ "\n",
+ "### CoreCrypto transactions\n",
+ "CoreCrypto has plans to create a transaction API so we can batch-decrypt messages, which should speed up the `decrypting` part.\n",
+ "\n",
+ "### Check \"Other\" slice\n",
+ "Perhaps there is still a bit of time to gain in the \"Other\" part of the code, which is related to Protobuf parsing, Base64 decoding, etc. ?\n",
+ "\n",
+ "### Dive deeper into each message type\n",
+ "Keep in mind that most of the events collected for the experiments above were Text events. Which seems to be one of the worst performing messages we process.\n",
+ "This is definitely the most demanding job, as it requires pretty much one investigation for Text, one for LastRead, one for Asset, one for Receipt, etc.\n",
+ "But might be one of the most fruitful ones, and strikes a good balance between a major refactor that would be with CoreCrypto transactions, and small / segmented improvements across the processing pipeline."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Kotlin",
+ "language": "kotlin",
+ "name": "kotlin"
+ },
+ "language_info": {
+ "name": "kotlin",
+ "version": "1.9.23",
+ "mimetype": "text/x-kotlin",
+ "file_extension": ".kt",
+ "pygments_lexer": "kotlin",
+ "codemirror_mode": "text/x-kotlin",
+ "nbconvert_exporter": ""
+ },
+ "ktnbPluginMetadata": {
+ "projectLibraries": []
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/docs/notebooks/event-performance-investigation/logs/InitialData.txt b/docs/notebooks/event-performance-investigation/logs/InitialData.txt
new file mode 100644
index 00000000000..bf35441bacd
--- /dev/null
+++ b/docs/notebooks/event-performance-investigation/logs/InitialData.txt
@@ -0,0 +1,152 @@
+
+Decrypting Proteus. CheckingSession: 8.873250ms; Decrypting: 63.703776ms; Saving Session: 30.135295ms
+Processing Application Message. LegalHold: 5.755086ms; Actually Handling: 130.706096ms; Post Handling: 143.717us
+349 Text
+
+Decrypting Proteus. CheckingSession: 10.304281ms; Decrypting: 36.299642ms; Saving Session: 21.362386ms
+Processing Application Message. LegalHold: 23.325888ms; Actually Handling: 52.317098ms; Post Handling: 47.323us
+150 Text
+
+Decrypting Proteus. CheckingSession: 19.705038ms; Decrypting: 25.850708ms; Saving Session: 49.176188ms
+Processing Application Message. LegalHold: 12.177897ms; Actually Handling: 100.877320ms; Post Handling: 58.146us
+240 Text
+
+Decrypting Proteus. CheckingSession: 10.535889ms; Decrypting: 30.623861ms; Saving Session: 50.635539ms
+Processing Application Message. LegalHold: 86.545207ms; Actually Handling: 141.545858ms; Post Handling: 212.321us
+347 Text
+
+Decrypting Proteus. CheckingSession: 40.843750ms; Decrypting: 77.642823ms; Saving Session: 21.599772ms
+Processing Application Message. LegalHold: 9.775106ms; Actually Handling: 106.065226ms; Post Handling: 45.207us
+268 Text
+
+Decrypting Proteus. CheckingSession: 5.257365ms; Decrypting: 20.301676ms; Saving Session: 46.290242ms
+Processing Application Message. LegalHold: 3.956584ms; Actually Handling: 99.363404ms; Post Handling: 55.868us
+182 Text
+
+Decrypting Proteus. CheckingSession: 9.790934ms; Decrypting: 20.166585ms; Saving Session: 20.536784ms
+Processing Application Message. LegalHold: 4.910481ms; Actually Handling: 41.863892ms; Post Handling: 43.742us
+112 Text
+
+Decrypting Proteus. CheckingSession: 37.533936ms; Decrypting: 117.194052ms; Saving Session: 34.765462ms
+Processing Application Message. LegalHold: 11.211955ms; Actually Handling: 142.946085ms; Post Handling: 62.215us
+363 Text
+
+Decrypting Proteus. CheckingSession: 129.018718ms; Decrypting: 61.166015ms; Saving Session: 12.040650ms
+Processing Application Message. LegalHold: 131.752482ms; Actually Handling: 124.796590ms; Post Handling: 46.672us
+481 Text
+
+Decrypting Proteus. CheckingSession: 9.000204ms; Decrypting: 69.963949ms; Saving Session: 19.456218ms
+Processing Application Message. LegalHold: 12.183798ms; Actually Handling: 144.756022ms; Post Handling: 8.200154ms
+299 Text
+
+Decrypting Proteus. CheckingSession: 34.256022ms; Decrypting: 106.964966ms; Saving Session: 57.150595ms
+Processing Application Message. LegalHold: 6.519572ms; Actually Handling: 179.368612ms; Post Handling: 44.678us
+400 Text
+
+Decrypting Proteus. CheckingSession: 16.447510ms; Decrypting: 10.732666ms; Saving Session: 76.608684ms
+Processing Application Message. LegalHold: 17.636882ms; Actually Handling: 106.291829ms; Post Handling: 42.358us
+242 Text
+
+Decrypting Proteus. CheckingSession: 7.767496ms; Decrypting: 85.712199ms; Saving Session: 56.541259ms
+Processing Application Message. LegalHold: 19.073527ms; Actually Handling: 112.489461ms; Post Handling: 45.288us
+322 Text
+
+Decrypting Proteus. CheckingSession: 11.468709ms; Decrypting: 60.700643ms; Saving Session: 13.350546ms
+Processing Application Message. LegalHold: 8.505982ms; Actually Handling: 50.083130ms; Post Handling: 45.532us
+150 Text
+
+Decrypting Proteus. CheckingSession: 6.188883ms; Decrypting: 13.053426ms; Saving Session: 85.124837ms
+Processing Application Message. LegalHold: 21.323039ms; Actually Handling: 52.064534ms; Post Handling: 44.271us
+183 Text
+
+Decrypting Proteus. CheckingSession: 13.070516ms; Decrypting: 28.716634ms; Saving Session: 30.298055ms
+Processing Application Message. LegalHold: 4.070516ms; Actually Handling: 62.070149ms; Post Handling: 46.346us
+161 Text
+
+Decrypting Proteus. CheckingSession: 6.138061ms; Decrypting: 12.006470ms; Saving Session: 14.416707ms
+Processing Application Message. LegalHold: 5.838745ms; Actually Handling: 40.329020ms; Post Handling: 45.084us
+104 Text
+
+Decrypting Proteus. CheckingSession: 18.136312ms; Decrypting: 56.142212ms; Saving Session: 10.411458ms
+Processing Application Message. LegalHold: 5.751913ms; Actually Handling: 79.311198ms; Post Handling: 86.67us
+178 Text
+
+Decrypting Proteus. CheckingSession: 5.159220ms; Decrypting: 22.406942ms; Saving Session: 43.825399ms
+Processing Application Message. LegalHold: 3.482463ms; Actually Handling: 78.667847ms; Post Handling: 46.102us
+159 Text
+
+Decrypting Proteus. CheckingSession: 22.073405ms; Decrypting: 15.910563ms; Saving Session: 18.681885ms
+Processing Application Message. LegalHold: 4.313762ms; Actually Handling: 49.198934ms; Post Handling: 42.603us
+127 Text
+
+Decrypting Proteus. CheckingSession: 7.837036ms; Decrypting: 11.566772ms; Saving Session: 25.379231ms
+Processing Application Message. LegalHold: 4.045329ms; Actually Handling: 119.658447ms; Post Handling: 45.573us
+174 Text
+
+Decrypting Proteus. CheckingSession: 37.247396ms; Decrypting: 50.347819ms; Saving Session: 31.310221ms
+Processing Application Message. LegalHold: 18.579997ms; Actually Handling: 139.728719ms; Post Handling: 90.698us
+306 Text
+
+Decrypting Proteus. CheckingSession: 6.503500ms; Decrypting: 10.227702ms; Saving Session: 17.202026ms
+Processing Application Message. LegalHold: 12.241333ms; Actually Handling: 84.598836ms; Post Handling: 43.294us
+161 Text
+
+Decrypting Proteus. CheckingSession: 7.555704ms; Decrypting: 77.212647ms; Saving Session: 60.606690ms
+Processing Application Message. LegalHold: 26.573812ms; Actually Handling: 55.140462ms; Post Handling: 57.862us
+233 Text
+
+Decrypting Proteus. CheckingSession: 6.635539ms; Decrypting: 76.914876ms; Saving Session: 12.664348ms
+Processing Application Message. LegalHold: 7.748047ms; Actually Handling: 104.782104ms; Post Handling: 45.044us
+218 Text
+
+Decrypting Proteus. CheckingSession: 9.785726ms; Decrypting: 17.814534ms; Saving Session: 10.872436ms
+Processing Application Message. LegalHold: 5.510823ms; Actually Handling: 30.021362ms; Post Handling: 46.021us
+86 Text
+
+Decrypting Proteus. CheckingSession: 7.540120ms; Decrypting: 10.186483ms; Saving Session: 8.016560ms
+Processing Application Message. LegalHold: 3.805827ms; Actually Handling: 87.553345ms; Post Handling: 45.899us
+122 Text
+
+Decrypting Proteus. CheckingSession: 14.270101ms; Decrypting: 28.638753ms; Saving Session: 92.611044ms
+Processing Application Message. LegalHold: 4.849487ms; Actually Handling: 106.860189ms; Post Handling: 61.442us
+313 Text
+
+Decrypting Proteus. CheckingSession: 18.783529ms; Decrypting: 14.807129ms; Saving Session: 35.545858ms
+Processing Application Message. LegalHold: 28.228353ms; Actually Handling: 144.122518ms; Post Handling: 44.719us
+261 Text
+
+Decrypting Proteus. CheckingSession: 9.662638ms; Decrypting: 10.893880ms; Saving Session: 32.210978ms
+Processing Application Message. LegalHold: 4.977783ms; Actually Handling: 105.997029ms; Post Handling: 45.451us
+201 Text
+
+Decrypting Proteus. CheckingSession: 58.855998ms; Decrypting: 15.815918ms; Saving Session: 46.989014ms
+Processing Application Message. LegalHold: 39.791626ms; Actually Handling: 101.596924ms; Post Handling: 45.573us
+286 Text
+
+Decrypting Proteus. CheckingSession: 48.868653ms; Decrypting: 82.024048ms; Saving Session: 15.148235ms
+Processing Application Message. LegalHold: 6.876343ms; Actually Handling: 72.648153ms; Post Handling: 44.027us
+275 Text
+
+Decrypting Proteus. CheckingSession: 4.992024ms; Decrypting: 34.686604ms; Saving Session: 17.456991ms
+Processing Application Message. LegalHold: 14.882975ms; Actually Handling: 86.396240ms; Post Handling: 47.282us
+164 Text
+
+Decrypting Proteus. CheckingSession: 27.943970ms; Decrypting: 28.186035ms; Saving Session: 18.758708ms
+Processing Application Message. LegalHold: 24.808106ms; Actually Handling: 92.472941ms; Post Handling: 69.824us
+203 Text
+
+Decrypting Proteus. CheckingSession: 27.895060ms; Decrypting: 66.050049ms; Saving Session: 30.500814ms
+Processing Application Message. LegalHold: 3.519571ms; Actually Handling: 191.166545ms; Post Handling: 47.893us
+325 Text
+
+Decrypting Proteus. CheckingSession: 48.456991ms; Decrypting: 90.494141ms; Saving Session: 57.134359ms
+Processing Application Message. LegalHold: 13.465983ms; Actually Handling: 161.665446ms; Post Handling: 41.748us
+378 Text
+
+Decrypting Proteus. CheckingSession: 44.470988ms; Decrypting: 53.007121ms; Saving Session: 49.168823ms
+Processing Application Message. LegalHold: 33.682983ms; Actually Handling: 122.440349ms; Post Handling: 43.538us
+337 Text
+
+Decrypting Proteus. CheckingSession: 12.033041ms; Decrypting: 35.181274ms; Saving Session: 9.400960ms
+Processing Application Message. LegalHold: 16.091227ms; Actually Handling: 79.925822ms; Post Handling: 44.108us
+158 Text
diff --git a/docs/notebooks/event-performance-investigation/logs/NotSavingSession.txt b/docs/notebooks/event-performance-investigation/logs/NotSavingSession.txt
new file mode 100644
index 00000000000..55f2a282b2d
--- /dev/null
+++ b/docs/notebooks/event-performance-investigation/logs/NotSavingSession.txt
@@ -0,0 +1,156 @@
+
+Decrypting Proteus. CheckingSession: 15.454061ms; Decrypting: 28.070231ms; Saving Session: 5.697us
+Processing Application Message. LegalHold: 14.933us; Actually Handling: 97.785929ms; Post Handling: 182.657us
+412 LastRead
+
+Decrypting Proteus. CheckingSession: 11.785157ms; Decrypting: 22.148885ms; Saving Session: 4.069us
+Processing Application Message. LegalHold: 10.174601ms; Actually Handling: 181.047770ms; Post Handling: 108.154us
+267 Text
+
+Decrypting Proteus. CheckingSession: 17.730428ms; Decrypting: 16.207683ms; Saving Session: 4.761us
+Processing Application Message. LegalHold: 9.501302ms; Actually Handling: 75.200358ms; Post Handling: 108.805us
+140 Text
+
+Decrypting Proteus. CheckingSession: 95.788696ms; Decrypting: 36.985514ms; Saving Session: 5.045us
+Processing Application Message. LegalHold: 5.578369ms; Actually Handling: 138.195842ms; Post Handling: 89.518us
+292 Text
+
+Decrypting Proteus. CheckingSession: 29.748698ms; Decrypting: 45.943482ms; Saving Session: 3.866us
+Processing Application Message. LegalHold: 4.784953ms; Actually Handling: 346.692790ms; Post Handling: 89.518us
+444 Text
+
+Decrypting Proteus. CheckingSession: 8.107707ms; Decrypting: 21.656494ms; Saving Session: 3.825us
+Processing Application Message. LegalHold: 14.723348ms; Actually Handling: 43.679484ms; Post Handling: 90.698us
+127 Text
+
+Decrypting Proteus. CheckingSession: 42.428589ms; Decrypting: 50.376099ms; Saving Session: 3.988us
+Processing Application Message. LegalHold: 5.829631ms; Actually Handling: 104.662761ms; Post Handling: 92.082us
+226 Text
+
+Decrypting Proteus. CheckingSession: 17.885702ms; Decrypting: 108.601074ms; Saving Session: 4.965us
+Processing Application Message. LegalHold: 33.264404ms; Actually Handling: 58.645386ms; Post Handling: 91.39us
+231 Text
+
+Decrypting Proteus. CheckingSession: 85.114421ms; Decrypting: 105.152222ms; Saving Session: 5.127us
+Processing Application Message. LegalHold: 4.807495ms; Actually Handling: 89.738851ms; Post Handling: 86.548us
+327 Text
+
+Decrypting Proteus. CheckingSession: 15.756429ms; Decrypting: 27.951905ms; Saving Session: 3.906us
+Processing Application Message. LegalHold: 9.168823ms; Actually Handling: 87.466471ms; Post Handling: 93.14us
+147 Text
+
+Decrypting Proteus. CheckingSession: 44.635172ms; Decrypting: 60.306600ms; Saving Session: 4.761us
+Processing Application Message. LegalHold: 4.242594ms; Actually Handling: 82.865194ms; Post Handling: 95.621us
+205 Text
+
+Decrypting Proteus. CheckingSession: 58.932658ms; Decrypting: 92.710083ms; Saving Session: 5.209us
+Processing Application Message. LegalHold: 10.136637ms; Actually Handling: 139.580038ms; Post Handling: 89.274us
+330 Text
+
+Decrypting Proteus. CheckingSession: 33.654093ms; Decrypting: 106.929199ms; Saving Session: 4.964us
+Processing Application Message. LegalHold: 6.297811ms; Actually Handling: 125.664958ms; Post Handling: 106.893us
+286 Text
+
+Decrypting Proteus. CheckingSession: 37.078735ms; Decrypting: 110.817546ms; Saving Session: 5.005us
+Processing Application Message. LegalHold: 28.246907ms; Actually Handling: 121.051514ms; Post Handling: 87.687us
+314 Text
+
+Decrypting Proteus. CheckingSession: 7.747396ms; Decrypting: 45.533691ms; Saving Session: 5.818us
+Processing Application Message. LegalHold: 9.863688ms; Actually Handling: 101.962199ms; Post Handling: 90.902us
+172 Text
+
+Decrypting Proteus. CheckingSession: 8.374227ms; Decrypting: 59.100220ms; Saving Session: 3.866us
+Processing Application Message. LegalHold: 4.148681ms; Actually Handling: 111.556722ms; Post Handling: 89.559us
+198 Text
+
+Decrypting Proteus. CheckingSession: 61.912313ms; Decrypting: 69.451416ms; Saving Session: 4.965us
+Processing Application Message. LegalHold: 5.938232ms; Actually Handling: 210.333130ms; Post Handling: 138.346us
+360 Text
+
+Decrypting Proteus. CheckingSession: 15.601196ms; Decrypting: 10.290812ms; Saving Session: 3.744us
+Processing Application Message. LegalHold: 7.292155ms; Actually Handling: 26.934814ms; Post Handling: 43.091us
+124 Text
+
+Decrypting Proteus. CheckingSession: 48.981486ms; Decrypting: 13.180949ms; Saving Session: 3.744us
+Processing Application Message. LegalHold: 43.387492ms; Actually Handling: 84.452922ms; Post Handling: 41.789us
+232 Text
+
+Decrypting Proteus. CheckingSession: 10.598593ms; Decrypting: 39.504395ms; Saving Session: 4.232us
+Processing Application Message. LegalHold: 5.250448ms; Actually Handling: 65.109619ms; Post Handling: 53.996us
+151 Text
+
+Decrypting Proteus. CheckingSession: 21.209838ms; Decrypting: 62.311402ms; Saving Session: 5.209us
+Processing Application Message. LegalHold: 6.484538ms; Actually Handling: 57.000326ms; Post Handling: 39.632us
+181 Text
+
+Decrypting Proteus. CheckingSession: 6.562581ms; Decrypting: 55.756226ms; Saving Session: 5.127us
+Processing Application Message. LegalHold: 5.546671ms; Actually Handling: 37.751709ms; Post Handling: 40.405us
+112 Text
+
+Decrypting Proteus. CheckingSession: 40.094686ms; Decrypting: 24.501017ms; Saving Session: 3.906us
+Processing Application Message. LegalHold: 4.171590ms; Actually Handling: 61.336955ms; Post Handling: 44.149us
+162 Text
+
+Decrypting Proteus. CheckingSession: 21.579956ms; Decrypting: 26.329590ms; Saving Session: 4.191us
+Processing Application Message. LegalHold: 4.704508ms; Actually Handling: 20.097859ms; Post Handling: 41.381us
+80 Text
+
+Decrypting Proteus. CheckingSession: 6.706909ms; Decrypting: 22.204875ms; Saving Session: 5.818us
+Processing Application Message. LegalHold: 9.896729ms; Actually Handling: 16.063802ms; Post Handling: 41.423us
+72 Text
+
+Decrypting Proteus. CheckingSession: 15.992961ms; Decrypting: 23.335937ms; Saving Session: 4.883us
+Processing Application Message. LegalHold: 6.042847ms; Actually Handling: 14.047811ms; Post Handling: 40.608us
+68 Text
+
+Decrypting Proteus. CheckingSession: 33.254761ms; Decrypting: 33.165324ms; Saving Session: 4.151us
+Processing Application Message. LegalHold: 10.563924ms; Actually Handling: 25.756714ms; Post Handling: 46.956us
+122 Text
+
+Decrypting Proteus. CheckingSession: 40.167928ms; Decrypting: 55.158122ms; Saving Session: 4.191us
+Processing Application Message. LegalHold: 7.033366ms; Actually Handling: 45.535278ms; Post Handling: 40.975us
+191 Text
+
+Decrypting Proteus. CheckingSession: 20.063924ms; Decrypting: 28.767172ms; Saving Session: 4.069us
+Processing Application Message. LegalHold: 4.934366ms; Actually Handling: 29.515666ms; Post Handling: 40.731us
+89 Text
+
+Decrypting Proteus. CheckingSession: 15.520548ms; Decrypting: 57.723185ms; Saving Session: 5.046us
+Processing Application Message. LegalHold: 4.028361ms; Actually Handling: 16.930623ms; Post Handling: 42.196us
+104 Text
+
+Decrypting Proteus. CheckingSession: 6.614787ms; Decrypting: 99.018148ms; Saving Session: 5.005us
+Processing Application Message. LegalHold: 40.461263ms; Actually Handling: 24.233317ms; Post Handling: 41.341us
+177 Text
+
+Decrypting Proteus. CheckingSession: 19.239502ms; Decrypting: 19.057088ms; Saving Session: 3.907us
+Processing Application Message. LegalHold: 6.788941ms; Actually Handling: 21.477051ms; Post Handling: 44.759us
+72 Text
+
+Decrypting Proteus. CheckingSession: 20.884278ms; Decrypting: 13.739380ms; Saving Session: 4.761us
+Processing Application Message. LegalHold: 19.949260ms; Actually Handling: 11.556518ms; Post Handling: 37.191us
+72 Text
+
+Decrypting Proteus. CheckingSession: 22.686971ms; Decrypting: 12.227987ms; Saving Session: 4.68us
+Processing Application Message. LegalHold: 4.054525ms; Actually Handling: 58.352865ms; Post Handling: 40.446us
+109 Text
+
+Decrypting Proteus. CheckingSession: 33.300741ms; Decrypting: 69.654378ms; Saving Session: 4.964us
+Processing Application Message. LegalHold: 4.356689ms; Actually Handling: 120.923299ms; Post Handling: 47.363us
+240 Text
+
+Decrypting Proteus. CheckingSession: 33.834310ms; Decrypting: 18.699748ms; Saving Session: 5.452us
+Processing Application Message. LegalHold: 4.842203ms; Actually Handling: 52.177408ms; Post Handling: 54.932us
+117 Text
+
+Decrypting Proteus. CheckingSession: 40.743897ms; Decrypting: 188.739014ms; Saving Session: 6.958us
+Processing Application Message. LegalHold: 79.312296ms; Actually Handling: 47.106241ms; Post Handling: 42.48us
+373 Text
+
+Decrypting Proteus. CheckingSession: 52.185547ms; Decrypting: 21.590617ms; Saving Session: 5.005us
+Processing Application Message. LegalHold: 10.031861ms; Actually Handling: 106.708211ms; Post Handling: 47.729us
+201 Text
+
+Decrypting Proteus. CheckingSession: 12.232300ms; Decrypting: 43.849935ms; Saving Session: 4.923us
+Processing Application Message. LegalHold: 25.414754ms; Actually Handling: 16.168091ms; Post Handling: 41.748us
+106 Text
diff --git a/docs/notebooks/event-performance-investigation/logs/WithExistingSessionCache.txt b/docs/notebooks/event-performance-investigation/logs/WithExistingSessionCache.txt
new file mode 100644
index 00000000000..f396d1b8a7b
--- /dev/null
+++ b/docs/notebooks/event-performance-investigation/logs/WithExistingSessionCache.txt
@@ -0,0 +1,208 @@
+
+Decrypting Proteus. CheckingSession: 171.63us; Decrypting: 23.412150ms; Saving Session: 4.72us
+Processing Application Message. LegalHold: 17.415us; Actually Handling: 30.606893ms; Post Handling: 61.157us
+65 LastRead
+
+Decrypting Proteus. CheckingSession: 130.615us; Decrypting: 23.862264ms; Saving Session: 4.761us
+Processing Application Message. LegalHold: 8.499105ms; Actually Handling: 87.421549ms; Post Handling: 47.689us
+128 Text
+
+Decrypting Proteus. CheckingSession: 132.447us; Decrypting: 40.465699ms; Saving Session: 4.883us
+Processing Application Message. LegalHold: 43.508220ms; Actually Handling: 84.078043ms; Post Handling: 43.498us
+191 Text
+
+Decrypting Proteus. CheckingSession: 97.819us; Decrypting: 35.966390ms; Saving Session: 3.947us
+Processing Application Message. LegalHold: 22.293782ms; Actually Handling: 82.308838ms; Post Handling: 41.015us
+147 Text
+
+Decrypting Proteus. CheckingSession: 109.823us; Decrypting: 67.539388ms; Saving Session: 4.76us
+Processing Application Message. LegalHold: 18.875651ms; Actually Handling: 39.499064ms; Post Handling: 41.422us
+143 Text
+
+Decrypting Proteus. CheckingSession: 88.786us; Decrypting: 34.672689ms; Saving Session: 4.435us
+Processing Application Message. LegalHold: 12.254029ms; Actually Handling: 59.550049ms; Post Handling: 42.155us
+112 Text
+
+Decrypting Proteus. CheckingSession: 100.057us; Decrypting: 56.516113ms; Saving Session: 4.476us
+Processing Application Message. LegalHold: 4.922648ms; Actually Handling: 43.885620ms; Post Handling: 40.935us
+133 Text
+
+Decrypting Proteus. CheckingSession: 99.203us; Decrypting: 76.217733ms; Saving Session: 5.168us
+Processing Application Message. LegalHold: 4.521729ms; Actually Handling: 74.523437ms; Post Handling: 65.186us
+168 Text
+
+Decrypting Proteus. CheckingSession: 146.891us; Decrypting: 41.369263ms; Saving Session: 5.94us
+Processing Application Message. LegalHold: 17.425049ms; Actually Handling: 160.854289ms; Post Handling: 369.913us
+251 Text
+
+Decrypting Proteus. CheckingSession: 156.779us; Decrypting: 69.674438ms; Saving Session: 5.331us
+Processing Application Message. LegalHold: 29.853068ms; Actually Handling: 32.974365ms; Post Handling: 44.556us
+180 Text
+
+Decrypting Proteus. CheckingSession: 97.412us; Decrypting: 194.863933ms; Saving Session: 4.721us
+Processing Application Message. LegalHold: 8.449300ms; Actually Handling: 110.715779ms; Post Handling: 47.607us
+343 Text
+
+Decrypting Proteus. CheckingSession: 143.88us; Decrypting: 63.876993ms; Saving Session: 4.72us
+Processing Application Message. LegalHold: 26.663046ms; Actually Handling: 100.153483ms; Post Handling: 43.01us
+200 Text
+
+Decrypting Proteus. CheckingSession: 102.051us; Decrypting: 33.846557ms; Saving Session: 3.988us
+Processing Application Message. LegalHold: 32.440105ms; Actually Handling: 77.101400ms; Post Handling: 50.13us
+171 Text
+
+Decrypting Proteus. CheckingSession: 90.82us; Decrypting: 82.648763ms; Saving Session: 8.22us
+Processing Application Message. LegalHold: 13.928873ms; Actually Handling: 59.246338ms; Post Handling: 74.911us
+164 Text
+
+Decrypting Proteus. CheckingSession: 81.95us; Decrypting: 20.931438ms; Saving Session: 3.377us
+Processing Application Message. LegalHold: 22.170573ms; Actually Handling: 54.304688ms; Post Handling: 60.628us
+104 Text
+
+Decrypting Proteus. CheckingSession: 112.955us; Decrypting: 30.412068ms; Saving Session: 3.621us
+Processing Application Message. LegalHold: 7.205160ms; Actually Handling: 56.475545ms; Post Handling: 48.869us
+102 Text
+
+Decrypting Proteus. CheckingSession: 85.368us; Decrypting: 36.424520ms; Saving Session: 5.005us
+Processing Application Message. LegalHold: 4.438477ms; Actually Handling: 30.431315ms; Post Handling: 40.771us
+77 Text
+
+Decrypting Proteus. CheckingSession: 80.892us; Decrypting: 55.061971ms; Saving Session: 4.517us
+Processing Application Message. LegalHold: 13.422851ms; Actually Handling: 23.873372ms; Post Handling: 40.243us
+103 Text
+
+Decrypting Proteus. CheckingSession: 90.047us; Decrypting: 115.897624ms; Saving Session: 5.045us
+Processing Application Message. LegalHold: 5.418457ms; Actually Handling: 63.461101ms; Post Handling: 41.667us
+191 Text
+
+Decrypting Proteus. CheckingSession: 80.933us; Decrypting: 67.734945ms; Saving Session: 4.394us
+Processing Application Message. LegalHold: 7.931397ms; Actually Handling: 45.393474ms; Post Handling: 39.795us
+129 Text
+
+Decrypting Proteus. CheckingSession: 96.436us; Decrypting: 55.728027ms; Saving Session: 6.144us
+Processing Application Message. LegalHold: 4.656291ms; Actually Handling: 82.777262ms; Post Handling: 55.502us
+204 Text
+
+Decrypting Proteus. CheckingSession: 134.074us; Decrypting: 31.648315ms; Saving Session: 3.662us
+Processing Application Message. LegalHold: 5.795695ms; Actually Handling: 41.531982ms; Post Handling: 40.894us
+117 Text
+
+Decrypting Proteus. CheckingSession: 125.529us; Decrypting: 32.789713ms; Saving Session: 4.882us
+Processing Application Message. LegalHold: 5.650310ms; Actually Handling: 23.350098ms; Post Handling: 41.178us
+73 Text
+
+Decrypting Proteus. CheckingSession: 83.821us; Decrypting: 101.161296ms; Saving Session: 4.639us
+Processing Application Message. LegalHold: 5.676758ms; Actually Handling: 84.787313ms; Post Handling: 42.643us
+198 Text
+
+Decrypting Proteus. CheckingSession: 101.929us; Decrypting: 100.994466ms; Saving Session: 4.476us
+Processing Application Message. LegalHold: 6.584229ms; Actually Handling: 44.870361ms; Post Handling: 43.701us
+165 Text
+
+Decrypting Proteus. CheckingSession: 101.44us; Decrypting: 40.139079ms; Saving Session: 5.656us
+Processing Application Message. LegalHold: 23.923584ms; Actually Handling: 73.660075ms; Post Handling: 40.161us
+190 Text
+
+Decrypting Proteus. CheckingSession: 97.575us; Decrypting: 13.363729ms; Saving Session: 3.459us
+Processing Application Message. LegalHold: 15.317383ms; Actually Handling: 19.124390ms; Post Handling: 45.573us
+78 Text
+
+Decrypting Proteus. CheckingSession: 114.461us; Decrypting: 18.798991ms; Saving Session: 4.435us
+Processing Application Message. LegalHold: 7.636108ms; Actually Handling: 95.518391ms; Post Handling: 62.418us
+172 Text
+
+Decrypting Proteus. CheckingSession: 89.396us; Decrypting: 45.923869ms; Saving Session: 5.046us
+Processing Application Message. LegalHold: 30.199504ms; Actually Handling: 31.469930ms; Post Handling: 57.617us
+124 Text
+
+Decrypting Proteus. CheckingSession: 117.187us; Decrypting: 68.765584ms; Saving Session: 6.348us
+Processing Application Message. LegalHold: 19.563192ms; Actually Handling: 70.637858ms; Post Handling: 68.074us
+172 Text
+
+Decrypting Proteus. CheckingSession: 206.543us; Decrypting: 110.533895ms; Saving Session: 9.684us
+Processing Application Message. LegalHold: 9.586548ms; Actually Handling: 19.422160ms; Post Handling: 44.719us
+155 Text
+
+Decrypting Proteus. CheckingSession: 84.798us; Decrypting: 28.744751ms; Saving Session: 4.558us
+Processing Application Message. LegalHold: 4.979573ms; Actually Handling: 18.377157ms; Post Handling: 39.998us
+74 Text
+
+Decrypting Proteus. CheckingSession: 178.101us; Decrypting: 14.005493ms; Saving Session: 3.337us
+Processing Application Message. LegalHold: 4.134807ms; Actually Handling: 30.131144ms; Post Handling: 38.492us
+54 Text
+
+Decrypting Proteus. CheckingSession: 92.407us; Decrypting: 70.199544ms; Saving Session: 4.679us
+Processing Application Message. LegalHold: 13.248739ms; Actually Handling: 114.933187ms; Post Handling: 39.998us
+259 Text
+
+Decrypting Proteus. CheckingSession: 159.424us; Decrypting: 41.670899ms; Saving Session: 4.313us
+Processing Application Message. LegalHold: 28.232829ms; Actually Handling: 16.727539ms; Post Handling: 36.621us
+103 Text
+
+Decrypting Proteus. CheckingSession: 92.529us; Decrypting: 64.359416ms; Saving Session: 4.028us
+Processing Application Message. LegalHold: 4.952107ms; Actually Handling: 14.338094ms; Post Handling: 54.932us
+89 Text
+
+Decrypting Proteus. CheckingSession: 101.888us; Decrypting: 49.372478ms; Saving Session: 5.249us
+Processing Application Message. LegalHold: 36.024455ms; Actually Handling: 41.159505ms; Post Handling: 43.905us
+168 Text
+
+Decrypting Proteus. CheckingSession: 85.205us; Decrypting: 14.965413ms; Saving Session: 4.028us
+Processing Application Message. LegalHold: 5.274007ms; Actually Handling: 32.183391ms; Post Handling: 44.271us
+58 Text
+
+Decrypting Proteus. CheckingSession: 92.082us; Decrypting: 86.730713ms; Saving Session: 5.086us
+Processing Application Message. LegalHold: 4.609131ms; Actually Handling: 38.390340ms; Post Handling: 45.166us
+135 Text
+
+Decrypting Proteus. CheckingSession: 101.725us; Decrypting: 15.713461ms; Saving Session: 3.662us
+Processing Application Message. LegalHold: 4.611979ms; Actually Handling: 47.136109ms; Post Handling: 50.904us
+92 Text
+
+Decrypting Proteus. CheckingSession: 86.1us; Decrypting: 74.593669ms; Saving Session: 6.755us
+Processing Application Message. LegalHold: 4.965617ms; Actually Handling: 26.958374ms; Post Handling: 41.015us
+136 Text
+
+Decrypting Proteus. CheckingSession: 94.523us; Decrypting: 26.139405ms; Saving Session: 3.58us
+Processing Application Message. LegalHold: 14.431519ms; Actually Handling: 46.557496ms; Post Handling: 3.227986ms
+97 Text
+
+Decrypting Proteus. CheckingSession: 149.211us; Decrypting: 38.674520ms; Saving Session: 3.58us
+Processing Application Message. LegalHold: 5.177002ms; Actually Handling: 76.428630ms; Post Handling: 78.125us
+145 Text
+
+Decrypting Proteus. CheckingSession: 115.845us; Decrypting: 18.225139ms; Saving Session: 4.476us
+Processing Application Message. LegalHold: 19.205403ms; Actually Handling: 76.934448ms; Post Handling: 62.622us
+145 Text
+
+Decrypting Proteus. CheckingSession: 103.231us; Decrypting: 53.127604ms; Saving Session: 5.005us
+Processing Application Message. LegalHold: 5.670289ms; Actually Handling: 56.225545ms; Post Handling: 58.675us
+151 Text
+
+Decrypting Proteus. CheckingSession: 82.885us; Decrypting: 115.505900ms; Saving Session: 8.789us
+Processing Application Message. LegalHold: 11.379720ms; Actually Handling: 99.572673ms; Post Handling: 122.192us
+263 Text
+
+Decrypting Proteus. CheckingSession: 183.756us; Decrypting: 104.482625ms; Saving Session: 6.714us
+Processing Application Message. LegalHold: 13.235555ms; Actually Handling: 78.640258ms; Post Handling: 54.525us
+204 Text
+
+Decrypting Proteus. CheckingSession: 111.206us; Decrypting: 36.096680ms; Saving Session: 6.307us
+Processing Application Message. LegalHold: 31.570028ms; Actually Handling: 24.436483ms; Post Handling: 46.712us
+160 Text
+
+Decrypting Proteus. CheckingSession: 79.671us; Decrypting: 92.863240ms; Saving Session: 4.965us
+Processing Application Message. LegalHold: 50.057739ms; Actually Handling: 69.449626ms; Post Handling: 61.482us
+221 Text
+
+Decrypting Proteus. CheckingSession: 1.046183ms; Decrypting: 72.940877ms; Saving Session: 5.208us
+Processing Application Message. LegalHold: 5.366862ms; Actually Handling: 11.162923ms; Post Handling: 38.615us
+116 Text
+
+Decrypting Proteus. CheckingSession: 71.37us; Decrypting: 22.764160ms; Saving Session: 4.191us
+Processing Application Message. LegalHold: 9.260538ms; Actually Handling: 45.517619ms; Post Handling: 43.132us
+92 Text
+
+Decrypting Proteus. CheckingSession: 80.119us; Decrypting: 40.078939ms; Saving Session: 4.476us
+Processing Application Message. LegalHold: 6.736613ms; Actually Handling: 83.116537ms; Post Handling: 42.521us
+138 Text
diff --git a/gradle.properties b/gradle.properties
index f49ce3e6c56..e5a644b6f8d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -27,3 +27,4 @@ org.gradle.parallel=true
# Support KMP Gradle Composite Builds - See https://youtrack.jetbrains.com/issue/KT-52172/
kotlin.mpp.import.enableKgpDependencyResolution=true
+kotlin.native.ignoreDisabledTargets=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ecb66216eff..7403b9b0317 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,11 +1,12 @@
[versions]
kotlin = "1.9.23"
+ksp = "1.9.23-1.0.20"
activity-compose = "1.9.0"
app-compat = "1.6.1"
android-paging3 = "3.2.1"
cli-kt = "3.5.0"
-coroutines = "1.8.0"
-compose-compiler = "1.5.11"
+coroutines = "1.8.1"
+compose-compiler = "1.5.13"
compose-ui = "1.6.6"
compose-material = "1.6.6"
cryptobox4j = "1.4.0"
@@ -16,12 +17,12 @@ okio = "3.9.0"
ok-http = "4.12.0"
mockative = "2.2.0"
android-work = "2.9.0"
-android-test-runner = "1.5.2"
-android-test-core-ktx = "1.5.0"
-android-test-rules = "1.5.0"
-android-test-core = "1.5.0"
+android-test-runner = "1.6.2"
+android-test-core-ktx = "1.6.1"
+android-test-rules = "1.6.1"
+android-test-core = "1.6.1"
androidx-arch = "2.2.0"
-androidx-test-orchestrator = "1.4.2"
+androidx-test-orchestrator = "1.5.1"
androidx-sqlite = "2.4.0"
benasher-uuid = "0.8.0"
ktx-datetime = { strictly = "0.5.0" }
@@ -36,20 +37,20 @@ sqldelight = "2.0.2"
sqlcipher-android = "4.5.6"
pbandk = "0.14.2"
turbine = "1.1.0"
-avs = "9.8.15"
+avs = "10.0.1"
jna = "5.14.0"
-core-crypto = "1.0.0-rc.56-hotfix.2"
+core-crypto = "1.0.2"
core-crypto-multiplatform = "0.6.0-rc.3-multiplatform-pre1"
completeKotlin = "1.1.0"
-desugar-jdk = "2.0.4"
+desugar-jdk = "2.1.3"
kermit = "2.0.3"
detekt = "1.23.6"
-agp = "8.3.2"
+agp = "8.5.2"
dokka = "1.8.20"
carthage = "0.0.1"
libsodiumBindings = "0.8.7"
protobufCodegen = "0.9.4"
-annotation = "1.7.1"
+annotation = "1.9.1"
mordant = "2.0.0-beta13"
apache-tika = "2.9.2"
mockk = "1.13.10"
@@ -66,6 +67,7 @@ benchmark = "0.4.10"
jmh = "1.37"
jmhReport = "0.9.6"
xerialDriver = "3.45.3.0"
+kotlinx-io = "0.5.3"
[plugins]
# Home-made convention plugins
@@ -78,7 +80,7 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
-ksp = { id = "com.google.devtools.ksp", version = "1.9.23-1.0.20" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
carthage = { id = "com.wire.carthage-gradle-plugin", version.ref = "carthage" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufCodegen" }
@@ -102,6 +104,7 @@ ktxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json"
ktxDateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "ktx-datetime" }
ktx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "ktx-atomicfu" }
ktxReactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "coroutines" }
+ktxIO = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io"}
# android dependencies
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "app-compat" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 7454180f2ae..e6441136f3d 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 74c71905115..2a551a737fa 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -18,6 +18,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionSha256Sum=f8b4f4772d302c8ff580bc40d0f56e715de69b163546944f787c87abf209c961
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index ca8867d1cca..b740cf13397 100755
--- a/gradlew
+++ b/gradlew
@@ -1,67 +1,115 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MSYS* | MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -71,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -82,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=`expr $i + 1`
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 107acd32c4e..25da30dbdee 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt b/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt
index 8e77dfb90ac..f029a6bf0df 100644
--- a/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt
+++ b/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt
@@ -259,7 +259,7 @@ class KaliumLogger(
enum class ApplicationFlow {
SYNC, EVENT_RECEIVER, CONVERSATIONS, CONNECTIONS, MESSAGES, SEARCH, SESSION, REGISTER,
- CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS
+ CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS, CONVERSATIONS_FOLDERS
}
}
}
diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt
index 7b87ab28bc6..2e89948aefd 100644
--- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt
+++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt
@@ -21,22 +21,26 @@ package com.wire.kalium.logic.feature.call
import com.wire.kalium.calling.Calling
import com.wire.kalium.calling.types.Handle
import com.wire.kalium.logic.cache.SelfConversationIdProvider
+import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.call.CallRepository
import com.wire.kalium.logic.data.call.VideoStateChecker
import com.wire.kalium.logic.data.call.mapper.CallMapperImpl
import com.wire.kalium.logic.data.conversation.ClientId
import com.wire.kalium.logic.data.conversation.ConversationRepository
import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.data.id.CurrentClientIdProvider
import com.wire.kalium.logic.data.id.FederatedIdMapper
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.data.message.Message
import com.wire.kalium.logic.data.message.MessageContent
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.data.user.UserRepository
-import com.wire.kalium.logic.data.id.CurrentClientIdProvider
+import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater
+import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider
import com.wire.kalium.logic.feature.message.MessageSender
import com.wire.kalium.logic.featureFlags.KaliumConfigs
import com.wire.kalium.logic.test_util.TestKaliumDispatcher
+import com.wire.kalium.network.NetworkStateObserver
import io.mockative.Mock
import io.mockative.any
import io.mockative.eq
@@ -44,14 +48,10 @@ import io.mockative.mock
import io.mockative.once
import io.mockative.verify
import kotlinx.coroutines.test.runTest
+import kotlinx.datetime.Instant
import kotlin.test.BeforeTest
import kotlin.test.Ignore
import kotlin.test.Test
-import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater
-import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider
-import com.wire.kalium.network.NetworkStateObserver
-import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString
-import kotlinx.datetime.Instant
class CallManagerTest {
@@ -79,6 +79,9 @@ class CallManagerTest {
@Mock
private val selfConversationIdProvider = mock(SelfConversationIdProvider::class)
+ @Mock
+ private val userConfigRepository = mock(UserConfigRepository::class)
+
@Mock
private val conversationRepository = mock(ConversationRepository::class)
@@ -117,6 +120,7 @@ class CallManagerTest {
currentClientIdProvider = currentClientIdProvider,
selfConversationIdProvider = selfConversationIdProvider,
conversationRepository = conversationRepository,
+ userConfigRepository = userConfigRepository,
messageSender = messageSender,
kaliumDispatchers = dispatcher,
federatedIdMapper = federatedIdMapper,
@@ -128,7 +132,7 @@ class CallManagerTest {
networkStateObserver = networkStateObserver,
kaliumConfigs = kaliumConfigs,
mediaManagerService = mediaManagerService,
- flowManagerService = flowManagerService,
+ flowManagerService = flowManagerService
)
}
diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt
index c2c58431013..1bb4efa23d1 100644
--- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt
+++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt
@@ -17,13 +17,13 @@
*/
package com.wire.kalium.logic.feature.call.scenario
-import com.wire.kalium.calling.types.Uint32_t
import com.wire.kalium.calling.ConversationTypeCalling
+import com.wire.kalium.calling.types.Uint32_t
import com.wire.kalium.logic.data.call.CallRepository
+import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.logic.data.call.ConversationTypeForCall
import com.wire.kalium.logic.data.call.mapper.CallMapperImpl
import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl
-import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.logic.featureFlags.KaliumConfigs
import com.wire.kalium.logic.framework.TestClient
import com.wire.kalium.logic.framework.TestConversation
@@ -33,11 +33,13 @@ import io.mockative.coVerify
import io.mockative.eq
import io.mockative.mock
import io.mockative.once
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
+@OptIn(ExperimentalCoroutinesApi::class)
class OnIncomingCallTest {
val testScope = TestScope()
@@ -64,7 +66,7 @@ class OnIncomingCallTest {
eq(TestConversation.CONVERSATION.id),
eq(ConversationTypeForCall.Conference),
eq(CallStatus.INCOMING),
- eq(TestUser.USER_ID.toString()),
+ eq(TestUser.USER_ID),
eq(true),
eq(false),
eq(false)
@@ -94,7 +96,7 @@ class OnIncomingCallTest {
eq(TestConversation.CONVERSATION.id),
eq(ConversationTypeForCall.Conference),
eq(CallStatus.STILL_ONGOING),
- eq(TestUser.USER_ID.toString()),
+ eq(TestUser.USER_ID),
eq(true),
eq(false),
eq(false)
diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt
new file mode 100644
index 00000000000..86834de9f89
--- /dev/null
+++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt
@@ -0,0 +1,21 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.feature.conversation
+
+val ConversationScope.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery
+ get() = GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository)
diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt
new file mode 100644
index 00000000000..61bf99999e3
--- /dev/null
+++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt
@@ -0,0 +1,44 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.feature.conversation
+
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
+import com.wire.kalium.logic.data.conversation.ConversationRepository
+import com.wire.kalium.logic.data.conversation.ConversationQueryConfig
+import com.wire.kalium.util.KaliumDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+
+/**
+ * This use case will observe and return a flow of paginated searched conversation details with last message and unread events counts.
+ * @see PagingData
+ * @see ConversationDetailsWithEvents
+ */
+class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase internal constructor(
+ private val dispatcher: KaliumDispatcher,
+ private val conversationRepository: ConversationRepository,
+) {
+ suspend operator fun invoke(
+ queryConfig: ConversationQueryConfig,
+ pagingConfig: PagingConfig,
+ startingOffset: Long,
+ ): Flow> = conversationRepository.extensions
+ .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, pagingConfig, startingOffset).flowOn(dispatcher.io)
+}
diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt
index 1fe832a2ef7..c7782670592 100644
--- a/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt
+++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt
@@ -22,7 +22,10 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
+import android.os.Build
+import com.wire.kalium.logger.KaliumLogLevel
import com.wire.kalium.logic.kaliumLogger
+import com.wire.kalium.logic.logStructuredJson
import com.wire.kalium.network.NetworkState
import com.wire.kalium.network.NetworkStateObserver
import com.wire.kalium.util.KaliumDispatcher
@@ -32,6 +35,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -55,7 +59,8 @@ internal actual class NetworkStateObserverImpl(
init {
val initialDefaultNetworkData = connectivityManager.activeNetwork?.let {
- val defaultNetworkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
+ val defaultNetworkCapabilities =
+ connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
DefaultNetworkData.Connected(it, defaultNetworkCapabilities)
} ?: DefaultNetworkData.NotConnected
defaultNetworkDataStateFlow = MutableStateFlow(initialDefaultNetworkData)
@@ -70,18 +75,38 @@ internal actual class NetworkStateObserverImpl(
else networkData.networkCapabilities.toState()
} else NetworkState.NotConnected
}
+ .buffer(capacity = 0)
.stateIn(scope, SharingStarted.Eagerly, initialState)
val callback = object : ConnectivityManager.NetworkCallback() {
- override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ override fun onCapabilitiesChanged(
+ network: Network,
+ networkCapabilities: NetworkCapabilities
+ ) {
super.onCapabilitiesChanged(network, networkCapabilities)
- kaliumLogger.i(
- "${NetworkStateObserver.TAG} capabilities changed " +
- "internet:${networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)} " +
- "validated:${networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)}"
- )
- defaultNetworkDataStateFlow.update { DefaultNetworkData.Connected(network, networkCapabilities) }
+ val loggerMessage = mutableMapOf().apply {
+ put("internet", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).toString())
+ put("validated", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED).toString())
+ put("not restricted", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED).toString())
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ put("foreground", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND).toString())
+ put("not congested", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED).toString())
+ put("not suspended", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED).toString())
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ put("signalStrength", networkCapabilities.signalStrength.toString())
+ }
+ }
+ kaliumLogger.logStructuredJson(KaliumLogLevel.INFO, "${NetworkStateObserver.TAG} capabilities changed", loggerMessage)
+ defaultNetworkDataStateFlow.update {
+ DefaultNetworkData.Connected(
+ network,
+ networkCapabilities
+ )
+ }
}
override fun onLost(network: Network) {
@@ -110,8 +135,18 @@ internal actual class NetworkStateObserverImpl(
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
kaliumLogger.i("${NetworkStateObserver.TAG} block connection changed to $blocked")
defaultNetworkDataStateFlow.update {
- if (it is DefaultNetworkData.Connected) it.copy(isBlocked = blocked)
- else it
+ val updatedValue = when (it) {
+ is DefaultNetworkData.Connected -> {
+ it.copy(isBlocked = blocked)
+ }
+
+ is DefaultNetworkData.NotConnected -> {
+ if (blocked) it
+ else DefaultNetworkData.Connected(network)
+ }
+ }
+ kaliumLogger.d("${NetworkStateObserver.TAG} default network state $it changed to $updatedValue")
+ updatedValue
}
super.onBlockedStatusChanged(network, blocked)
}
diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt
index 8d024920776..b2cfe894414 100644
--- a/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt
+++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt
@@ -24,7 +24,7 @@ import androidx.annotation.DrawableRes
* Provide resources that will be displayed when Kalium
* needs to display a Foreground notification due to some work being done.
*/
-interface ForegroundNotificationDetailsProvider {
+fun interface ForegroundNotificationDetailsProvider {
@DrawableRes
fun getSmallIconResId(): Int
}
diff --git a/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt
new file mode 100644
index 00000000000..31ace8e2c65
--- /dev/null
+++ b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.feature.conversation
+
+import androidx.paging.PagingData
+import app.cash.paging.PagingConfig
+import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
+import com.wire.kalium.logic.data.conversation.ConversationQueryConfig
+import com.wire.kalium.logic.data.conversation.ConversationRepository
+import com.wire.kalium.logic.data.conversation.ConversationRepositoryExtensions
+import com.wire.kalium.logic.test_util.TestKaliumDispatcher
+import io.mockative.Mock
+import io.mockative.any
+import io.mockative.coEvery
+import io.mockative.coVerify
+import io.mockative.every
+import io.mockative.mock
+import io.mockative.once
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest {
+ private val dispatcher = TestKaliumDispatcher
+
+ @Test
+ fun givenSearchQuery_whenGettingPaginatedList_thenCallUseCaseWithProperParams() = runTest(dispatcher.default) {
+ // Given
+ val (arrangement, useCase) = Arrangement().withPaginatedConversationResult(emptyFlow()).arrange()
+ with(arrangement) {
+ // When
+ useCase(queryConfig = queryConfig, pagingConfig = pagingConfig, startingOffset = startingOffset)
+ // Then
+ coVerify {
+ conversationRepository.extensions
+ .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, pagingConfig, startingOffset)
+ }.wasInvoked(exactly = once)
+ }
+ }
+
+ inner class Arrangement {
+ @Mock
+ val conversationRepository = mock(ConversationRepository::class)
+
+ @Mock
+ val conversationRepositoryExtensions = mock(ConversationRepositoryExtensions::class)
+
+ val queryConfig = ConversationQueryConfig("search")
+ val pagingConfig = PagingConfig(20)
+ val startingOffset = 0L
+
+ init {
+ every {
+ conversationRepository.extensions
+ }.returns(conversationRepositoryExtensions)
+ }
+
+ suspend fun withPaginatedConversationResult(result: Flow>) = apply {
+ coEvery {
+ conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery(any(), any(), any())
+ }.returns(result)
+ }
+
+ fun arrange() = this to GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository)
+ }
+}
diff --git a/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt
index 3dbc414f131..ecda6d43e7c 100644
--- a/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt
+++ b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt
@@ -229,6 +229,21 @@ class NetworkStateObserverImplTest {
}
}
+ @Test
+ fun givenNetworkNotConnectedAndButBlocked_whenItChangesToNotBlocked_thenStateChangesToConnectedWithInternet() =
+ runTest(dispatcher.default) {
+ // given
+ val (arrangement, networkStateObserverImpl) = Arrangement()
+ .arrange()
+ // when-then
+ networkStateObserverImpl.observeNetworkState().test {
+ assertEquals(NetworkState.NotConnected, awaitItem())
+ arrangement.connectNetwork(networkType = NetworkType.MOBILE, setAsDefault = true, withInternetValidated = true)
+ arrangement.changeNetworkBlocked(networkType = NetworkType.MOBILE, false)
+ assertEquals(NetworkState.ConnectedWithInternet, awaitItem())
+ }
+ }
+
@Test
fun givenOneNetworkConnectedWithoutInternetValidated_whenItChangesToBlocked_thenStateDoesNotChange() = runTest(dispatcher.default) {
// given
diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt
index f2a4229f634..1382845bf18 100644
--- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt
+++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt
@@ -21,16 +21,17 @@
package com.wire.kalium.logic.feature.call
import com.wire.kalium.logic.cache.SelfConversationIdProvider
+import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.call.CallRepository
import com.wire.kalium.logic.data.call.VideoStateChecker
import com.wire.kalium.logic.data.call.mapper.CallMapper
import com.wire.kalium.logic.data.conversation.ConversationRepository
-import com.wire.kalium.logic.data.user.UserId
+import com.wire.kalium.logic.data.id.CurrentClientIdProvider
import com.wire.kalium.logic.data.id.FederatedIdMapper
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.QualifiedIdMapper
+import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.data.user.UserRepository
-import com.wire.kalium.logic.data.id.CurrentClientIdProvider
import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater
import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider
import com.wire.kalium.logic.feature.message.MessageSender
@@ -46,6 +47,7 @@ actual class GlobalCallManager {
currentClientIdProvider: CurrentClientIdProvider,
selfConversationIdProvider: SelfConversationIdProvider,
conversationRepository: ConversationRepository,
+ userConfigRepository: UserConfigRepository,
messageSender: MessageSender,
callMapper: CallMapper,
federatedIdMapper: FederatedIdMapper,
diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt
index d4ba1b1e45e..a41535a1dc3 100644
--- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt
+++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt
@@ -32,6 +32,7 @@ actual fun mapMLSException(exception: Exception): MLSFailure =
is CryptoError.StaleCommit -> MLSFailure.StaleCommit
is CryptoError.ConversationAlreadyExists -> MLSFailure.ConversationAlreadyExists
is CryptoError.MessageEpochTooOld -> MLSFailure.MessageEpochTooOld
+ is CryptoError.MlsException -> MLSFailure.InternalErrors
else -> MLSFailure.Generic(exception)
}
} else {
diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt
index 9af3912b90a..90244f7eb76 100644
--- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt
+++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt
@@ -32,8 +32,10 @@ import com.wire.kalium.calling.types.Uint32_t
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.cache.SelfConversationIdProvider
import com.wire.kalium.logic.callingLogger
+import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.call.CallClient
import com.wire.kalium.logic.data.call.CallClientList
+import com.wire.kalium.logic.data.call.CallHelperImpl
import com.wire.kalium.logic.data.call.CallRepository
import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.logic.data.call.CallType
@@ -76,6 +78,8 @@ import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvide
import com.wire.kalium.logic.feature.message.MessageSender
import com.wire.kalium.logic.featureFlags.KaliumConfigs
import com.wire.kalium.logic.functional.fold
+import com.wire.kalium.logic.util.ServerTimeHandler
+import com.wire.kalium.logic.util.ServerTimeHandlerImpl
import com.wire.kalium.logic.util.toInt
import com.wire.kalium.network.NetworkStateObserver
import com.wire.kalium.util.KaliumDispatcher
@@ -111,11 +115,13 @@ class CallManagerImpl internal constructor(
private val conversationClientsInCallUpdater: ConversationClientsInCallUpdater,
private val networkStateObserver: NetworkStateObserver,
private val getCallConversationType: GetCallConversationTypeProvider,
+ private val userConfigRepository: UserConfigRepository,
private val kaliumConfigs: KaliumConfigs,
private val mediaManagerService: MediaManagerService,
private val flowManagerService: FlowManagerService,
private val json: Json = Json { ignoreUnknownKeys = true },
private val shouldRemoteMuteChecker: ShouldRemoteMuteChecker = ShouldRemoteMuteCheckerImpl(),
+ private val serverTimeHandler: ServerTimeHandler = ServerTimeHandlerImpl(),
kaliumDispatchers: KaliumDispatcher = KaliumDispatcherImpl
) : CallManager {
@@ -209,8 +215,12 @@ class CallManagerImpl internal constructor(
.keepingStrongReference(),
establishedCallHandler = OnEstablishedCall(callRepository, scope, qualifiedIdMapper)
.keepingStrongReference(),
- closeCallHandler = OnCloseCall(callRepository, scope, qualifiedIdMapper, networkStateObserver)
- .keepingStrongReference(),
+ closeCallHandler = OnCloseCall(
+ callRepository = callRepository,
+ networkStateObserver = networkStateObserver,
+ scope = scope,
+ qualifiedIdMapper = qualifiedIdMapper
+ ).keepingStrongReference(),
metricsHandler = metricsHandler,
callConfigRequestHandler = OnConfigRequest(calling, callRepository, scope)
.keepingStrongReference(),
@@ -248,8 +258,6 @@ class CallManagerImpl internal constructor(
)
if (callingValue.type != REMOTE_MUTE_TYPE || shouldRemoteMute) {
- val currTime = System.currentTimeMillis()
-
val targetConversationId = if (message.isSelfMessage) {
content.conversationId ?: message.conversationId
} else {
@@ -263,7 +271,7 @@ class CallManagerImpl internal constructor(
inst = deferredHandle.await(),
msg = msg,
len = msg.size,
- curr_time = Uint32_t(value = currTime / 1000),
+ curr_time = Uint32_t(value = serverTimeHandler.toServerTimestamp()),
msg_time = Uint32_t(value = message.date.epochSeconds),
convId = federatedIdMapper.parseToFederatedId(targetConversationId),
userId = federatedIdMapper.parseToFederatedId(message.senderUserId),
@@ -295,7 +303,7 @@ class CallManagerImpl internal constructor(
isMuted = false,
isCameraOn = isCameraOn,
isCbrEnabled = isAudioCbr,
- callerId = userId.await().toString()
+ callerId = userId.await()
)
withCalling {
@@ -455,14 +463,19 @@ class CallManagerImpl internal constructor(
callClients: CallClientList
) {
withCalling {
- // Needed to support calls between federated and non federated environments
+ // Mapping Needed to support calls between federated and non federated environments (domain separation)
val clients = callClients.clients.map { callClient ->
CallClient(
- federatedIdMapper.parseToFederatedId(callClient.userId),
- callClient.clientId
+ userId = federatedIdMapper.parseToFederatedId(callClient.userId),
+ clientId = callClient.clientId,
+ isMemberOfSubconversation = callClient.isMemberOfSubconversation,
+ quality = callClient.quality
)
}
val clientsJson = CallClientList(clients).toJsonString()
+ callingLogger.d(
+ "$TAG - wcall_request_video_streams() called -> Requesting video streams for conversation = ${conversationId.toLogString()}"
+ )
val conversationIdString = federatedIdMapper.parseToFederatedId(conversationId)
calling.wcall_request_video_streams(
inst = it,
@@ -532,6 +545,9 @@ class CallManagerImpl internal constructor(
callRepository = callRepository,
qualifiedIdMapper = qualifiedIdMapper,
participantMapper = ParticipantMapperImpl(videoStateChecker, callMapper, qualifiedIdMapper),
+ userConfigRepository = userConfigRepository,
+ callHelper = CallHelperImpl(),
+ endCall = { endCall(it) },
callingScope = scope
).keepingStrongReference()
diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt
index f88f5494be8..277eadc1662 100644
--- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt
+++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt
@@ -26,6 +26,7 @@ import com.wire.kalium.calling.ENVIRONMENT_DEFAULT
import com.wire.kalium.calling.callbacks.LogHandler
import com.wire.kalium.logic.cache.SelfConversationIdProvider
import com.wire.kalium.logic.callingLogger
+import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.call.CallRepository
import com.wire.kalium.logic.data.call.VideoStateChecker
import com.wire.kalium.logic.data.call.mapper.CallMapper
@@ -41,6 +42,7 @@ import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvide
import com.wire.kalium.logic.feature.message.MessageSender
import com.wire.kalium.logic.featureFlags.KaliumConfigs
import com.wire.kalium.logic.util.CurrentPlatform
+import com.wire.kalium.logic.util.DummyCallManager
import com.wire.kalium.logic.util.PlatformContext
import com.wire.kalium.logic.util.PlatformType
import com.wire.kalium.network.NetworkStateObserver
@@ -83,6 +85,7 @@ actual class GlobalCallManager(
currentClientIdProvider: CurrentClientIdProvider,
selfConversationIdProvider: SelfConversationIdProvider,
conversationRepository: ConversationRepository,
+ userConfigRepository: UserConfigRepository,
messageSender: MessageSender,
callMapper: CallMapper,
federatedIdMapper: FederatedIdMapper,
@@ -93,26 +96,31 @@ actual class GlobalCallManager(
networkStateObserver: NetworkStateObserver,
kaliumConfigs: KaliumConfigs
): CallManager {
- return callManagerHolder.computeIfAbsent(userId) {
- CallManagerImpl(
- calling = calling,
- callRepository = callRepository,
- userRepository = userRepository,
- currentClientIdProvider = currentClientIdProvider,
- selfConversationIdProvider = selfConversationIdProvider,
- callMapper = callMapper,
- messageSender = messageSender,
- conversationRepository = conversationRepository,
- federatedIdMapper = federatedIdMapper,
- qualifiedIdMapper = qualifiedIdMapper,
- videoStateChecker = videoStateChecker,
- conversationClientsInCallUpdater = conversationClientsInCallUpdater,
- getCallConversationType = getCallConversationType,
- networkStateObserver = networkStateObserver,
- mediaManagerService = mediaManager,
- flowManagerService = flowManager,
- kaliumConfigs = kaliumConfigs
- )
+ if (kaliumConfigs.enableCalling) {
+ return callManagerHolder.computeIfAbsent(userId) {
+ CallManagerImpl(
+ calling = calling,
+ callRepository = callRepository,
+ userRepository = userRepository,
+ currentClientIdProvider = currentClientIdProvider,
+ selfConversationIdProvider = selfConversationIdProvider,
+ callMapper = callMapper,
+ messageSender = messageSender,
+ conversationRepository = conversationRepository,
+ federatedIdMapper = federatedIdMapper,
+ qualifiedIdMapper = qualifiedIdMapper,
+ videoStateChecker = videoStateChecker,
+ conversationClientsInCallUpdater = conversationClientsInCallUpdater,
+ getCallConversationType = getCallConversationType,
+ networkStateObserver = networkStateObserver,
+ mediaManagerService = mediaManager,
+ flowManagerService = flowManager,
+ userConfigRepository = userConfigRepository,
+ kaliumConfigs = kaliumConfigs
+ )
+ }
+ } else {
+ return DummyCallManager()
}
}
diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt
index cee73c451e4..26d8c0b8194 100644
--- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt
+++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt
@@ -27,10 +27,10 @@ import com.wire.kalium.calling.types.Uint32_t
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.callingLogger
import com.wire.kalium.logic.data.call.CallRepository
+import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedIdMapper
-import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.network.NetworkState
import com.wire.kalium.network.NetworkStateObserver
import kotlinx.coroutines.CoroutineScope
@@ -63,8 +63,13 @@ class OnCloseCall(
scope.launch {
- val isConnectedToInternet = networkStateObserver.observeNetworkState().value == NetworkState.ConnectedWithInternet
- if (shouldPersistMissedCall(conversationIdWithDomain, callStatus) && isConnectedToInternet) {
+ val isConnectedToInternet =
+ networkStateObserver.observeNetworkState().value == NetworkState.ConnectedWithInternet
+ if (shouldPersistMissedCall(
+ conversationIdWithDomain,
+ callStatus
+ ) && isConnectedToInternet
+ ) {
callRepository.persistMissedCall(conversationIdWithDomain)
}
@@ -76,12 +81,14 @@ class OnCloseCall(
if (callRepository.getCallMetadataProfile()[conversationIdWithDomain]?.protocol is Conversation.ProtocolInfo.MLS) {
callRepository.leaveMlsConference(conversationIdWithDomain)
}
-
callingLogger.i("[OnCloseCall] -> ConversationId: ${conversationId.obfuscateId()} | callStatus: $callStatus")
}
}
- private fun shouldPersistMissedCall(conversationId: ConversationId, callStatus: CallStatus): Boolean {
+ private fun shouldPersistMissedCall(
+ conversationId: ConversationId,
+ callStatus: CallStatus
+ ): Boolean {
if (callStatus == CallStatus.MISSED)
return true
return callRepository.getCallMetadataProfile().data[conversationId]?.let {
diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt
index 4c23e8fe868..25b68968352 100644
--- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt
+++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt
@@ -62,7 +62,7 @@ class OnIncomingCall(
callRepository.createCall(
conversationId = qualifiedConversationId,
status = status,
- callerId = qualifiedIdMapper.fromStringToQualifiedID(userId).toString(),
+ callerId = qualifiedIdMapper.fromStringToQualifiedID(userId),
isMuted = isMuted,
isCameraOn = false,
type = mappedConversationType,
diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt
index 1ffa9c6c950..a9836ef78dc 100644
--- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt
+++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt
@@ -23,11 +23,17 @@ import com.wire.kalium.calling.callbacks.ParticipantChangedHandler
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.callingLogger
import com.wire.kalium.logic.data.call.CallParticipants
+import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.call.CallRepository
+import com.wire.kalium.logic.data.call.CallHelper
import com.wire.kalium.logic.data.call.ParticipantMinimized
import com.wire.kalium.logic.data.call.mapper.ParticipantMapper
+import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedIdMapper
+import com.wire.kalium.logic.functional.getOrElse
+import com.wire.kalium.logic.kaliumLogger
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
@@ -36,21 +42,45 @@ class OnParticipantListChanged internal constructor(
private val callRepository: CallRepository,
private val qualifiedIdMapper: QualifiedIdMapper,
private val participantMapper: ParticipantMapper,
- private val callingScope: CoroutineScope
+ private val userConfigRepository: UserConfigRepository,
+ private val callHelper: CallHelper,
+ private val endCall: suspend (conversationId: ConversationId) -> Unit,
+ private val callingScope: CoroutineScope,
+ private val jsonDecoder: Json = Json
) : ParticipantChangedHandler {
override fun onParticipantChanged(remoteConversationId: String, data: String, arg: Pointer?) {
- val participantsChange = Json.decodeFromString(data)
+ val participantsChange = jsonDecoder.decodeFromString(data)
callingScope.launch {
val participants = mutableListOf()
- val conversationIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(remoteConversationId)
+ val conversationIdWithDomain =
+ qualifiedIdMapper.fromStringToQualifiedID(remoteConversationId)
participantsChange.members.map { member ->
participants.add(participantMapper.fromCallMemberToParticipantMinimized(member))
}
+ if (userConfigRepository.shouldUseSFTForOneOnOneCalls().getOrElse(false)) {
+ val callProtocol = callRepository.currentCallProtocol(conversationIdWithDomain)
+
+ val currentCall = callRepository.establishedCallsFlow().first().firstOrNull()
+ currentCall?.let {
+ val shouldEndSFTOneOnOneCall = callHelper.shouldEndSFTOneOnOneCall(
+ conversationId = conversationIdWithDomain,
+ callProtocol = callProtocol,
+ conversationType = it.conversationType,
+ newCallParticipants = participants,
+ previousCallParticipants = it.participants
+ )
+ if (shouldEndSFTOneOnOneCall) {
+ kaliumLogger.i("[onParticipantChanged] - Ending SFT one on one call due to participant leaving")
+ endCall(conversationIdWithDomain)
+ }
+ }
+ }
+
callRepository.updateCallParticipants(
conversationId = conversationIdWithDomain,
participants = participants
diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/util/DummyCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/util/DummyCallManager.kt
new file mode 100644
index 00000000000..017ceeb5a4d
--- /dev/null
+++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/util/DummyCallManager.kt
@@ -0,0 +1,69 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.util
+
+import com.wire.kalium.calling.ConversationTypeCalling
+import com.wire.kalium.logic.data.call.CallClientList
+import com.wire.kalium.logic.data.call.CallType
+import com.wire.kalium.logic.data.call.EpochInfo
+import com.wire.kalium.logic.data.call.Participant
+import com.wire.kalium.logic.data.call.TestVideoType
+import com.wire.kalium.logic.data.call.VideoState
+import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.data.message.Message
+import com.wire.kalium.logic.data.message.MessageContent
+import com.wire.kalium.logic.feature.call.CallManager
+
+@Suppress("EmptyFunctionBlock", "TooManyFunctions")
+class DummyCallManager : CallManager {
+ override suspend fun onCallingMessageReceived(message: Message.Signaling, content: MessageContent.Calling) {}
+
+ override suspend fun startCall(
+ conversationId: ConversationId,
+ callType: CallType,
+ conversationTypeCalling: ConversationTypeCalling,
+ isAudioCbr: Boolean
+ ) {
+ }
+
+ override suspend fun answerCall(conversationId: ConversationId, isAudioCbr: Boolean) {}
+
+ override suspend fun endCall(conversationId: ConversationId) {}
+
+ override suspend fun rejectCall(conversationId: ConversationId) {}
+
+ override suspend fun muteCall(shouldMute: Boolean) {}
+
+ override suspend fun setVideoSendState(conversationId: ConversationId, videoState: VideoState) {}
+
+ override suspend fun requestVideoStreams(conversationId: ConversationId, callClients: CallClientList) {}
+
+ override suspend fun updateEpochInfo(conversationId: ConversationId, epochInfo: EpochInfo) {}
+
+ override suspend fun updateConversationClients(conversationId: ConversationId, clients: String) {}
+
+ override suspend fun reportProcessNotifications(isStarted: Boolean) {}
+
+ override suspend fun setTestVideoType(testVideoType: TestVideoType) {}
+
+ override suspend fun setTestPreviewActive(shouldEnable: Boolean) {}
+
+ override suspend fun setTestRemoteVideoStates(conversationId: ConversationId, participants: List) {}
+
+ override suspend fun cancelJobs() {}
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt
index 6ce16825226..24e28261290 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt
@@ -21,6 +21,7 @@ package com.wire.kalium.logic
import com.wire.kalium.cryptography.exceptions.ProteusException
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.functional.Either
+import com.wire.kalium.logic.functional.onFailure
import com.wire.kalium.network.exceptions.APINotSupported
import com.wire.kalium.network.exceptions.KaliumException
import com.wire.kalium.network.exceptions.isFederationDenied
@@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
sealed interface CoreFailure {
@@ -208,6 +210,7 @@ interface MLSFailure : CoreFailure {
data object ConversationDoesNotSupportMLS : MLSFailure
data object StaleProposal : MLSFailure
data object StaleCommit : MLSFailure
+ data object InternalErrors : MLSFailure
data class Generic(internal val exception: Exception) : MLSFailure {
val rootCause: Throwable get() = exception
@@ -336,7 +339,7 @@ internal inline fun wrapProteusRequest(proteusRequest: () -> T): Eithe
|"cause": ${e.cause} }" """.trimMargin()
)
kaliumLogger.e(e.stackTraceToString())
- Either.Left(ProteusFailure(ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause)))
+ Either.Left(ProteusFailure(ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause)))
}
}
@@ -367,12 +370,13 @@ internal inline fun wrapE2EIRequest(e2eiRequest: () -> T): Either wrapStorageRequest(storageRequest: () -> T?): Either {
- return try {
+ val result = try {
storageRequest()?.let { data -> Either.Right(data) } ?: Either.Left(StorageFailure.DataNotFound)
} catch (e: Exception) {
- kaliumLogger.e(e.stackTraceToString())
Either.Left(StorageFailure.Generic(e))
}
+ result.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) }
+ return result
}
/**
@@ -403,8 +407,9 @@ internal fun Flow.wrapStorageRequest(): Flow Either.Right(data) } ?: Either.Left(StorageFailure.DataNotFound)
}.catch { e ->
- kaliumLogger.e(e.stackTraceToString())
emit(Either.Left(StorageFailure.Generic(e)))
+ }.onEach {
+ it.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) }
}
internal inline fun wrapFlowStorageRequest(storageRequest: () -> Flow): Flow> {
@@ -412,12 +417,12 @@ internal inline fun wrapFlowStorageRequest(storageRequest: () -> Flow<
storageRequest().map {
it?.let { data -> Either.Right(data) } ?: Either.Left(StorageFailure.DataNotFound)
}.catch { e ->
- kaliumLogger.e(e.stackTraceToString())
emit(Either.Left(StorageFailure.Generic(e)))
}
} catch (e: Exception) {
- kaliumLogger.e(e.stackTraceToString())
flowOf(Either.Left(StorageFailure.Generic(e)))
+ }.onEach {
+ it.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) }
}
}
@@ -426,12 +431,12 @@ internal inline fun wrapNullableFlowStorageRequest(storageRequest: ()
storageRequest().map {
Either.Right(it) as Either
}.catch { e ->
- kaliumLogger.e(e.stackTraceToString())
- emit(Either.Left(StorageFailure.Generic(e)))
+ emit(Either.Left(StorageFailure.Generic(e)))
}
} catch (e: Exception) {
- kaliumLogger.e(e.stackTraceToString())
- flowOf(Either.Left(StorageFailure.Generic(e)))
+ flowOf(Either.Left(StorageFailure.Generic(e)))
+ }.onEach {
+ it.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) }
}
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt
index 51b0d969358..61f6fa6d038 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt
@@ -93,6 +93,7 @@ interface UserConfigRepository {
suspend fun getSupportedProtocols(): Either>
fun setConferenceCallingEnabled(enabled: Boolean): Either
fun isConferenceCallingEnabled(): Either
+ fun observeConferenceCallingEnabled(): Flow>
fun setUseSFTForOneOnOneCalls(shouldUse: Boolean): Either
fun shouldUseSFTForOneOnOneCalls(): Either
fun setSecondFactorPasswordChallengeStatus(isRequired: Boolean): Either
@@ -136,14 +137,16 @@ interface UserConfigRepository {
suspend fun setShouldNotifyForRevokedCertificate(shouldNotify: Boolean)
suspend fun observeShouldNotifyForRevokedCertificate(): Flow>
suspend fun clearE2EISettings()
- fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean)
- fun getShouldFetchE2EITrustAnchor(): Boolean
suspend fun setCurrentTrackingIdentifier(newIdentifier: String)
suspend fun getCurrentTrackingIdentifier(): String?
suspend fun observeCurrentTrackingIdentifier(): Flow>
suspend fun setPreviousTrackingIdentifier(identifier: String)
suspend fun getPreviousTrackingIdentifier(): String?
suspend fun deletePreviousTrackingIdentifier()
+ suspend fun updateNextTimeForCallFeedback(valueMs: Long)
+ suspend fun getNextTimeForCallFeedback(): Either
+ suspend fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean)
+ suspend fun getShouldFetchE2EITrustAnchor(): Boolean
}
@Suppress("TooManyFunctions")
@@ -300,6 +303,9 @@ internal class UserConfigDataSource internal constructor(
userConfigStorage.isConferenceCallingEnabled()
}
+ override fun observeConferenceCallingEnabled(): Flow> =
+ userConfigStorage.isConferenceCallingEnabledFlow().wrapStorageRequest()
+
override fun setUseSFTForOneOnOneCalls(shouldUse: Boolean): Either = wrapStorageRequest {
userConfigStorage.persistUseSftForOneOnOneCalls(shouldUse)
}
@@ -506,12 +512,10 @@ internal class UserConfigDataSource internal constructor(
override suspend fun observeShouldNotifyForRevokedCertificate(): Flow> =
userConfigDAO.observeShouldNotifyForRevokedCertificate().wrapStorageRequest()
- override fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) {
- userConfigStorage.setShouldFetchE2EITrustAnchors(shouldFetch = shouldFetch)
+ override suspend fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) {
+ userConfigDAO.setShouldFetchE2EITrustAnchors(shouldFetch = shouldFetch)
}
- override fun getShouldFetchE2EITrustAnchor(): Boolean = userConfigStorage.getShouldFetchE2EITrustAnchorHasRun()
-
override suspend fun setCurrentTrackingIdentifier(newIdentifier: String) {
wrapStorageRequest {
userConfigDAO.setTrackingIdentifier(identifier = newIdentifier)
@@ -538,4 +542,12 @@ internal class UserConfigDataSource internal constructor(
userConfigDAO.deletePreviousTrackingIdentifier()
}
}
+
+ override suspend fun updateNextTimeForCallFeedback(valueMs: Long) {
+ userConfigDAO.setNextTimeForCallFeedback(valueMs)
+ }
+
+ override suspend fun getNextTimeForCallFeedback() = wrapStorageRequest { userConfigDAO.getNextTimeForCallFeedback() }
+
+ override suspend fun getShouldFetchE2EITrustAnchor(): Boolean = userConfigDAO.getShouldFetchE2EITrustAnchorHasRun()
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt
index 149493abf25..3bb8a0273a8 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt
@@ -134,6 +134,19 @@ data class ServerConfig(
isOnPremises = false,
apiProxy = null
)
+
+ val DUMMY = Links(
+ api = """https://dummy-nginz-https.zinfra.io""",
+ accounts = """https://wire-account-dummy.zinfra.io""",
+ webSocket = """https://dummy-nginz-ssl.zinfra.io""",
+ teams = """https://wire-teams-dummy.zinfra.io""",
+ blackList = """https://clientblacklist.wire.com/dummy""",
+ website = """https://dummy.wire.com""",
+ title = "dummy",
+ isOnPremises = false,
+ apiProxy = null
+ )
+
val DEFAULT = PRODUCTION
private const val FORGOT_PASSWORD_PATH = "forgot"
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt
index 54f10b8df38..faef6688735 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt
@@ -31,15 +31,18 @@ import com.wire.kalium.logic.functional.fold
import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.logic.wrapStorageRequest
-import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO
+import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi.Companion.MIN_API_VERSION
import com.wire.kalium.network.api.base.unbound.versioning.VersionApi
+import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO
import com.wire.kalium.persistence.daokaliumdb.ServerConfigurationDAO
import com.wire.kalium.util.KaliumDispatcher
import com.wire.kalium.util.KaliumDispatcherImpl
import io.ktor.http.Url
import kotlinx.coroutines.withContext
-internal interface ServerConfigRepository {
+interface ServerConfigRepository {
+ val minimumApiVersionForPersonalToTeamAccountMigration: Int
+
suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either
suspend fun storeConfig(links: ServerConfig.Links, metadata: ServerConfig.MetaData): Either
@@ -62,6 +65,7 @@ internal interface ServerConfigRepository {
* Return the server links and metadata for the given userId
*/
suspend fun configForUser(userId: UserId): Either
+ suspend fun commonApiVersion(domain: String): Either
}
@Suppress("LongParameterList", "TooManyFunctions")
@@ -72,6 +76,8 @@ internal class ServerConfigDataSource(
private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl
) : ServerConfigRepository {
+ override val minimumApiVersionForPersonalToTeamAccountMigration = MIN_API_VERSION
+
override suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either =
wrapStorageRequest { dao.configByLinks(serverConfigMapper.toEntity(serverLinks)) }.fold({
fetchApiVersionAndStore(serverLinks)
@@ -127,13 +133,17 @@ internal class ServerConfigDataSource(
}
override suspend fun updateConfigApiVersion(serverConfig: ServerConfig): Either =
- fetchMetadata(serverConfig.links)
- .flatMap { wrapStorageRequest { dao.updateApiVersion(serverConfig.id, it.commonApiVersion.version) } }
+ fetchMetadata(serverConfig.links)
+ .flatMap { wrapStorageRequest { dao.updateApiVersion(serverConfig.id, it.commonApiVersion.version) } }
override suspend fun configForUser(userId: UserId): Either =
wrapStorageRequest { dao.configForUser(userId.toDao()) }
.map { serverConfigMapper.fromEntity(it) }
+ override suspend fun commonApiVersion(domain: String): Either = wrapStorageRequest {
+ dao.getCommonApiVersion(domain)
+ }
+
private suspend fun fetchMetadata(serverLinks: ServerConfig.Links): Either =
wrapApiRequest { versionApi.fetchApiVersion(Url(serverLinks.api)) }
.flatMap {
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt
index db04024050d..a8eb1976618 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt
@@ -156,29 +156,33 @@ internal class AssetDataSource(
otrKey: AES256Key,
extension: String?
): Either> {
-
- val tempEncryptedDataPath = kaliumFileSystem.tempFilePath("${assetDataPath.name}.aes")
- val assetDataSource = kaliumFileSystem.source(assetDataPath)
- val assetDataSink = kaliumFileSystem.sink(tempEncryptedDataPath)
-
- // Encrypt the data on the provided temp path
- val encryptedDataSize = encryptFileWithAES256(assetDataSource, otrKey, assetDataSink)
- val encryptedDataSource = kaliumFileSystem.source(tempEncryptedDataPath)
-
- // Calculate the SHA of the encrypted data
- val sha256 = calcFileSHA256(encryptedDataSource)
- assetDataSink.close()
- encryptedDataSource.close()
- assetDataSource.close()
-
- val encryptionSucceeded = (encryptedDataSize > 0L && sha256 != null)
-
- return if (encryptionSucceeded) {
- val uploadAssetData = UploadAssetData(tempEncryptedDataPath, encryptedDataSize, mimeType, false, RetentionType.PERSISTENT)
- uploadAndPersistAsset(uploadAssetData, assetDataPath, extension).map { it to SHA256Key(sha256!!) }
- } else {
- kaliumLogger.e("Something went wrong when encrypting the Asset Message")
- Either.Left(EncryptionFailure.GenericEncryptionError)
+ try {
+ val tempEncryptedDataPath = kaliumFileSystem.tempFilePath("${assetDataPath.name}.aes")
+ val assetDataSource = kaliumFileSystem.source(assetDataPath)
+ val assetDataSink = kaliumFileSystem.sink(tempEncryptedDataPath)
+
+ // Encrypt the data on the provided temp path
+ val encryptedDataSize = encryptFileWithAES256(assetDataSource, otrKey, assetDataSink)
+ val encryptedDataSource = kaliumFileSystem.source(tempEncryptedDataPath)
+
+ // Calculate the SHA of the encrypted data
+ val sha256 = calcFileSHA256(encryptedDataSource)
+ assetDataSink.close()
+ encryptedDataSource.close()
+ assetDataSource.close()
+
+ val encryptionSucceeded = (encryptedDataSize > 0L && sha256 != null)
+
+ return if (encryptionSucceeded) {
+ val uploadAssetData = UploadAssetData(tempEncryptedDataPath, encryptedDataSize, mimeType, false, RetentionType.PERSISTENT)
+ uploadAndPersistAsset(uploadAssetData, assetDataPath, extension).map { it to SHA256Key(sha256!!) }
+ } else {
+ kaliumLogger.e("Something went wrong when encrypting the Asset Message")
+ Either.Left(EncryptionFailure.GenericEncryptionError)
+ }
+ } catch (e: IOException) {
+ kaliumLogger.e("Something went wrong when uploading the Asset Message. $e")
+ return Either.Left(CoreFailure.Unknown(e))
}
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallHelper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallHelper.kt
new file mode 100644
index 00000000000..710633ab3e5
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallHelper.kt
@@ -0,0 +1,74 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.call
+
+import com.wire.kalium.logic.data.conversation.Conversation
+import com.wire.kalium.logic.data.id.ConversationId
+
+/**
+ * Helper class to handle call related operations.
+ */
+interface CallHelper {
+
+ /**
+ * Check if the OneOnOne call that uses SFT should be ended.
+ * For Proteus, the call should be ended if the call has one participant after having 2 in the call.
+ * For MLS, the call should be ended if the call has two participants and the second participant has lost audio.
+ *
+ * @param conversationId the conversation id.
+ * @param callProtocol the call protocol.
+ * @param conversationType the conversation type.
+ * @param newCallParticipants the new call participants.
+ * @param previousCallParticipants the previous call participants.
+ * @return true if the call should be ended, false otherwise.
+ */
+ fun shouldEndSFTOneOnOneCall(
+ conversationId: ConversationId,
+ callProtocol: Conversation.ProtocolInfo?,
+ conversationType: Conversation.Type,
+ newCallParticipants: List,
+ previousCallParticipants: List
+ ): Boolean
+}
+
+class CallHelperImpl : CallHelper {
+
+ override fun shouldEndSFTOneOnOneCall(
+ conversationId: ConversationId,
+ callProtocol: Conversation.ProtocolInfo?,
+ conversationType: Conversation.Type,
+ newCallParticipants: List,
+ previousCallParticipants: List
+ ): Boolean {
+ return if (callProtocol is Conversation.ProtocolInfo.Proteus) {
+ conversationType == Conversation.Type.ONE_ON_ONE &&
+ newCallParticipants.size == ONE_PARTICIPANTS &&
+ previousCallParticipants.size == TWO_PARTICIPANTS
+ } else {
+ conversationType == Conversation.Type.ONE_ON_ONE &&
+ newCallParticipants.size == TWO_PARTICIPANTS &&
+ previousCallParticipants.size == TWO_PARTICIPANTS &&
+ previousCallParticipants[1].hasEstablishedAudio && !newCallParticipants[1].hasEstablishedAudio
+ }
+ }
+
+ companion object {
+ const val TWO_PARTICIPANTS = 2
+ const val ONE_PARTICIPANTS = 1
+ }
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt
index 722a6b9f07b..f45ce704881 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt
@@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.FederatedIdMapper
import com.wire.kalium.logic.data.id.GroupID
import com.wire.kalium.logic.data.id.QualifiedClientID
+import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.data.id.SubconversationId
import com.wire.kalium.logic.data.id.toCrypto
@@ -81,6 +82,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flattenConcat
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
@@ -108,7 +110,7 @@ interface CallRepository {
conversationId: ConversationId,
type: ConversationTypeForCall,
status: CallStatus,
- callerId: String,
+ callerId: UserId,
isMuted: Boolean,
isCameraOn: Boolean,
isCbrEnabled: Boolean
@@ -122,6 +124,7 @@ interface CallRepository {
fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: Map>)
suspend fun getLastClosedCallCreatedByConversationId(conversationId: ConversationId): Flow
suspend fun updateOpenCallsToClosedStatus()
+ suspend fun leavePreviouslyJoinedMlsConferences()
suspend fun persistMissedCall(conversationId: ConversationId)
suspend fun joinMlsConference(
conversationId: ConversationId,
@@ -131,6 +134,8 @@ interface CallRepository {
suspend fun leaveMlsConference(conversationId: ConversationId)
suspend fun observeEpochInfo(conversationId: ConversationId): Either>
suspend fun advanceEpoch(conversationId: ConversationId)
+ fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo?
+ suspend fun observeCurrentCall(conversationId: ConversationId): Flow
}
@Suppress("LongParameterList", "TooManyFunctions")
@@ -160,6 +165,26 @@ internal class CallDataSource(
private val callJobs = ConcurrentMutableMap()
private val staleParticipantJobs = ConcurrentMutableMap()
+ override suspend fun observeCurrentCall(conversationId: ConversationId): Flow = _callMetadataProfile.map {
+ it[conversationId]?.let { currentCall ->
+ Call(
+ conversationId = conversationId,
+ status = currentCall.callStatus,
+ isMuted = currentCall.isMuted,
+ isCameraOn = currentCall.isCameraOn,
+ isCbrEnabled = currentCall.isCbrEnabled,
+ callerId = currentCall.callerId,
+ conversationName = currentCall.conversationName,
+ conversationType = currentCall.conversationType,
+ callerName = currentCall.callerName,
+ callerTeamName = currentCall.callerTeamName,
+ establishedTime = currentCall.establishedTime,
+ participants = currentCall.getFullParticipants(),
+ maxParticipants = currentCall.maxParticipants
+ )
+ }
+ }
+
override suspend fun getCallConfigResponse(limit: Int?): Either = wrapApiRequest {
callApi.getCallConfig(limit = limit)
}
@@ -189,7 +214,7 @@ internal class CallDataSource(
conversationId: ConversationId,
type: ConversationTypeForCall,
status: CallStatus,
- callerId: String,
+ callerId: UserId,
isMuted: Boolean,
isCameraOn: Boolean,
isCbrEnabled: Boolean
@@ -197,10 +222,7 @@ internal class CallDataSource(
val conversation: ConversationDetails =
conversationRepository.observeConversationDetailsById(conversationId).onlyRight().first()
- // in OnIncomingCall we get callerId without a domain,
- // to cover that case and have a valid UserId we have that workaround
- val callerIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(callerId)
- val caller = userRepository.getKnownUser(callerIdWithDomain).first()
+ val caller = userRepository.getKnownUser(callerId).first()
val team = caller?.teamId?.let { teamId -> teamRepository.getTeam(teamId).first() }
val callEntity = callMapper.toCallEntity(
@@ -209,10 +231,11 @@ internal class CallDataSource(
type = type,
status = status,
conversationType = conversation.conversation.type,
- callerId = callerIdWithDomain
+ callerId = callerId
)
val metadata = CallMetadata(
+ callerId = callerId,
conversationName = conversation.conversation.name,
conversationType = conversation.conversation.type,
callerName = caller?.name,
@@ -419,6 +442,8 @@ internal class CallDataSource(
val currentParticipantIds = call.participants.map { it.userId }.toSet()
val newParticipantIds = participants.map { it.userId }.toSet()
+ val sharingScreenParticipantIds = participants.filter { it.isSharingScreen }
+ .map { participant -> participant.id }
val updatedUsers = call.users.toMutableList()
@@ -434,7 +459,11 @@ internal class CallDataSource(
this[conversationId] = call.copy(
participants = participants,
maxParticipants = max(call.maxParticipants, participants.size + 1),
- users = updatedUsers
+ users = updatedUsers,
+ screenShareMetadata = updateScreenSharingMetadata(
+ metadata = call.screenShareMetadata,
+ usersCurrentlySharingScreen = sharingScreenParticipantIds
+ )
)
}
@@ -444,7 +473,9 @@ internal class CallDataSource(
}
}
- if (_callMetadataProfile.value[conversationId]?.protocol is Conversation.ProtocolInfo.MLS) {
+ if (_callMetadataProfile.value[conversationId]?.protocol is Conversation.ProtocolInfo.MLS &&
+ _callMetadataProfile.value[conversationId]?.conversationType == Conversation.Type.GROUP
+ ) {
participants.forEach { participant ->
if (participant.hasEstablishedAudio) {
clearStaleParticipantTimeout(participant)
@@ -455,6 +486,43 @@ internal class CallDataSource(
}
}
+ /**
+ * Manages call sharing metadata for analytical purposes by tracking the following:
+ * - **Active Screen Shares**: Maintains a record of currently active screen shares with their start times (local to the device).
+ * - **Completed Screen Share Duration**: Accumulates the total duration of screen shares that have already ended.
+ * - **Unique Sharing Users**: Keeps a unique list of all users who have shared their screen during the call.
+ *
+ * To update the metadata, the following steps are performed:
+ * 1. **Calculate Ended Screen Share Time**: Determine the total time for users who stopped sharing since the last update.
+ * 2. **Update Active Shares**: Filter out inactive shares and add any new ones, associating them with the current start time.
+ * 3. **Track Unique Users**: Append ids to current set in order to keep track of unique users.
+ */
+ private fun updateScreenSharingMetadata(
+ metadata: CallScreenSharingMetadata,
+ usersCurrentlySharingScreen: List
+ ): CallScreenSharingMetadata {
+ val now = DateTimeUtil.currentInstant()
+
+ val alreadyEndedScreenSharesTimeInMillis = metadata.activeScreenShares
+ .filterKeys { id -> id !in usersCurrentlySharingScreen }
+ .values
+ .sumOf { startTime -> DateTimeUtil.calculateMillisDifference(startTime, now) }
+
+ val updatedShares = metadata.activeScreenShares
+ .filterKeys { id -> id in usersCurrentlySharingScreen }
+ .plus(
+ usersCurrentlySharingScreen
+ .filterNot { id -> metadata.activeScreenShares.containsKey(id) }
+ .associateWith { now }
+ )
+
+ return metadata.copy(
+ activeScreenShares = updatedShares,
+ completedScreenShareDurationInMillis = metadata.completedScreenShareDurationInMillis + alreadyEndedScreenSharesTimeInMillis,
+ uniqueSharingUsers = metadata.uniqueSharingUsers.plus(usersCurrentlySharingScreen.map { id -> id.toString() })
+ )
+ }
+
private fun clearStaleParticipantTimeout(participant: ParticipantMinimized) {
callingLogger.i("Clear stale participant timer")
val qualifiedClient = QualifiedClientID(ClientId(participant.clientId), participant.id)
@@ -552,7 +620,7 @@ internal class CallDataSource(
}
}
- private suspend fun leavePreviouslyJoinedMlsConferences() {
+ override suspend fun leavePreviouslyJoinedMlsConferences() {
callingLogger.i("Leaving previously joined MLS conferences")
callDAO.observeEstablishedCalls()
@@ -678,6 +746,9 @@ internal class CallDataSource(
} ?: callingLogger.w("[CallRepository] -> Requested new epoch but there's no conference subconversation")
}
+ override fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo? =
+ _callMetadataProfile.value.data[conversationId]?.protocol
+
companion object {
val STALE_PARTICIPANT_TIMEOUT = 190.toDuration(kotlin.time.DurationUnit.SECONDS)
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt
index 19f18747811..caba61b1818 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt
@@ -180,7 +180,7 @@ class CallMapperImpl(
isMuted = metadata?.isMuted ?: true,
isCameraOn = metadata?.isCameraOn ?: false,
isCbrEnabled = metadata?.isCbrEnabled ?: false,
- callerId = callEntity.callerId,
+ callerId = metadata?.callerId ?: qualifiedIdMapper.fromStringToQualifiedID(callEntity.callerId),
conversationName = metadata?.conversationName,
conversationType = toConversationType(conversationType = callEntity.conversationType),
callerName = metadata?.callerName,
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt
index 8cca1a5da58..eb84c333743 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt
@@ -76,15 +76,29 @@ interface ClientRepository {
suspend fun isClientRegistrationBlockedByE2EI(): Either
suspend fun deleteClient(param: DeleteClientParam): Either
suspend fun selfListOfClients(): Either>
- suspend fun observeClientsByUserIdAndClientId(userId: UserId, clientId: ClientId): Flow>
+ suspend fun observeClientsByUserIdAndClientId(
+ userId: UserId,
+ clientId: ClientId
+ ): Flow>
+
suspend fun storeUserClientListAndRemoveRedundantClients(clients: List): Either
- suspend fun storeUserClientIdList(userId: UserId, clients: List): Either
+ suspend fun storeUserClientIdList(
+ userId: UserId,
+ clients: List
+ ): Either
+
suspend fun storeMapOfUserToClientId(userToClientMap: Map>): Either
suspend fun removeClientsAndReturnUsersWithNoClients(
redundantClientsOfUsers: Map>
): Either>
- suspend fun registerToken(body: PushTokenBody): Either
+ suspend fun registerToken(
+ senderId: String,
+ client: String,
+ token: String,
+ transport: String
+ ): Either
+
suspend fun deregisterToken(token: String): Either
suspend fun getClientsByUserId(userId: UserId): Either>
suspend fun observeClientsByUserId(userId: UserId): Flow>>
@@ -205,7 +219,10 @@ class ClientDataSource(
}
}
- override suspend fun observeClientsByUserIdAndClientId(userId: UserId, clientId: ClientId): Flow> =
+ override suspend fun observeClientsByUserIdAndClientId(
+ userId: UserId,
+ clientId: ClientId
+ ): Flow> =
clientDAO.observeClient(userId.toDao(), clientId.value)
.map { it?.let { clientMapper.fromClientEntity(it) } }
.wrapStorageRequest()
@@ -250,7 +267,11 @@ class ClientDataSource(
redundantClientsOfUsers.mapKeys { it.key.toDao() }
.mapValues { it.value.map { clientId -> clientId.value } }
.let { redundantClientsOfUsersDao ->
- wrapStorageRequest { clientDAO.removeClientsAndReturnUsersWithNoClients(redundantClientsOfUsersDao) }
+ wrapStorageRequest {
+ clientDAO.removeClientsAndReturnUsersWithNoClients(
+ redundantClientsOfUsersDao
+ )
+ }
.map {
it.map { userId -> userId.toModel() }
}
@@ -258,10 +279,20 @@ class ClientDataSource(
override suspend fun storeUserClientListAndRemoveRedundantClients(
clients: List
- ): Either = wrapStorageRequest { clientDAO.insertClientsAndRemoveRedundant(clients) }
-
- override suspend fun registerToken(body: PushTokenBody): Either = clientRemoteRepository.registerToken(body)
- override suspend fun deregisterToken(token: String): Either = clientRemoteRepository.deregisterToken(token)
+ ): Either =
+ wrapStorageRequest { clientDAO.insertClientsAndRemoveRedundant(clients) }
+
+ override suspend fun registerToken(
+ senderId: String,
+ client: String,
+ token: String,
+ transport: String
+ ): Either = clientRemoteRepository.registerToken(
+ PushTokenBody(senderId, client, token, transport)
+ )
+
+ override suspend fun deregisterToken(token: String): Either =
+ clientRemoteRepository.deregisterToken(token)
override suspend fun getClientsByUserId(userId: UserId): Either> =
wrapStorageRequest {
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt
index 18369dcda3d..97d2dc0afe8 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt
@@ -141,9 +141,12 @@ class MLSClientProviderImpl(
override suspend fun clearLocalFiles() {
mlsClientMutex.withLock {
- mlsClient?.close()
- mlsClient = null
- FileUtil.deleteDirectory(rootKeyStorePath)
+ coreCryptoCentralMutex.withLock {
+ mlsClient?.close()
+ mlsClient = null
+ coreCryptoCentral = null
+ FileUtil.deleteDirectory(rootKeyStorePath)
+ }
}
}
@@ -182,7 +185,10 @@ class MLSClientProviderImpl(
}
}
- private suspend fun mlsClient(userId: CryptoUserID, clientId: ClientId): Either {
+ private suspend fun mlsClient(
+ userId: CryptoUserID,
+ clientId: ClientId
+ ): Either {
return getCoreCrypto(clientId).flatMap { cc ->
getOrFetchMLSConfig().map { (supportedCipherSuite, defaultCipherSuite) ->
cc.mlsClient(
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt
index 013deac601f..26f2f6ece6d 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt
@@ -18,9 +18,11 @@
package com.wire.kalium.logic.data.client
+import com.wire.kalium.cryptography.CoreCryptoCentral
import com.wire.kalium.cryptography.ProteusClient
import com.wire.kalium.cryptography.coreCryptoCentral
import com.wire.kalium.cryptography.cryptoboxProteusClient
+import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException
import com.wire.kalium.logger.KaliumLogLevel
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.CoreFailure
@@ -58,20 +60,15 @@ class ProteusClientProviderImpl(
private val userId: UserId,
private val passphraseStorage: PassphraseStorage,
private val kaliumConfigs: KaliumConfigs,
- private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl
+ private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl,
+ private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler
) : ProteusClientProvider {
private var _proteusClient: ProteusClient? = null
private val mutex = Mutex()
override suspend fun clearLocalFiles() {
- mutex.withLock {
- withContext(dispatcher.io) {
- _proteusClient?.close()
- _proteusClient = null
- FileUtil.deleteDirectory(rootProteusPath)
- }
- }
+ mutex.withLock { removeLocalFiles() }
}
override suspend fun getOrCreate(): ProteusClient {
@@ -109,7 +106,6 @@ class ProteusClientProviderImpl(
databaseKey = SecurityHelperImpl(passphraseStorage).proteusDBSecret(userId).value
)
} catch (e: Exception) {
-
val logMap = mapOf(
"userId" to userId.value.obfuscateId(),
"exception" to e,
@@ -119,7 +115,7 @@ class ProteusClientProviderImpl(
kaliumLogger.logStructuredJson(KaliumLogLevel.ERROR, TAG, logMap)
throw e
}
- central.proteusClient()
+ getCentralProteusClientOrError(central)
} else {
cryptoboxProteusClient(
rootDir = rootProteusPath,
@@ -129,6 +125,34 @@ class ProteusClientProviderImpl(
}
}
+ private suspend fun getCentralProteusClientOrError(central: CoreCryptoCentral): ProteusClient {
+ return try {
+ central.proteusClient()
+ } catch (exception: ProteusStorageMigrationException) {
+ proteusMigrationRecoveryHandler.clearClientData { removeLocalFiles() }
+ val logMap = mapOf(
+ "userId" to userId.value.obfuscateId(),
+ "exception" to exception,
+ "message" to exception.message,
+ "stackTrace" to exception.stackTraceToString()
+ )
+ kaliumLogger.withTextTag(TAG).logStructuredJson(KaliumLogLevel.ERROR, "Proteus storage migration failed", logMap)
+ throw exception
+ }
+ }
+
+ /**
+ * Actually deletes the proteus local files.
+ * Important! It is the caller responsibility to use the mutex, DON'T add a mutex here or it will be dead lock it.
+ */
+ private suspend fun removeLocalFiles() {
+ withContext(dispatcher.io) {
+ _proteusClient?.close()
+ _proteusClient = null
+ FileUtil.deleteDirectory(rootProteusPath)
+ }
+ }
+
private companion object {
const val TAG = "ProteusClientProvider"
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt
new file mode 100644
index 00000000000..9fe6880ecc4
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt
@@ -0,0 +1,30 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.client
+
+import com.wire.kalium.logic.data.logout.LogoutReason
+
+/**
+ * Handles the migration error of a proteus client storage from CryptoBox to CoreCrypto.
+ * It will perform a logout, using [LogoutReason.MIGRATION_TO_CC_FAILED] as the reason.
+ *
+ * This achieves that the client data is cleared and the user is logged out without losing content.
+ */
+interface ProteusMigrationRecoveryHandler {
+ suspend fun clearClientData(clearLocalFiles: suspend () -> Unit)
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt
index d8185faf11c..da27400302b 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt
@@ -154,6 +154,7 @@ internal class ConversationGroupRepositoryImpl(
val conversationEntity = conversationMapper.fromApiModelToDaoModel(
conversationResponse, mlsGroupState = ConversationEntity.GroupState.PENDING_CREATION, selfTeamId
)
+ val mlsPublicKeys = conversationMapper.fromApiModel(conversationResponse.publicKeys)
val protocol = protocolInfoMapper.fromEntity(conversationEntity.protocolInfo)
return wrapStorageRequest {
@@ -166,7 +167,8 @@ internal class ConversationGroupRepositoryImpl(
is Conversation.ProtocolInfo.MLSCapable -> mlsConversationRepository.establishMLSGroup(
groupID = protocol.groupId,
members = usersList + selfUserId,
- allowSkippingUsersWithoutKeyPackages = true
+ publicKeys = mlsPublicKeys,
+ allowSkippingUsersWithoutKeyPackages = true,
).map { it.notAddedUsers }
}
}.flatMap { protocolSpecificAdditionFailures ->
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt
index 59a63218fce..cc2d9995a75 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt
@@ -27,7 +27,9 @@ import com.wire.kalium.logic.data.id.TeamId
import com.wire.kalium.logic.data.id.toApi
import com.wire.kalium.logic.data.id.toDao
import com.wire.kalium.logic.data.id.toModel
-import com.wire.kalium.logic.data.message.MessagePreview
+import com.wire.kalium.logic.data.message.MessageMapper
+import com.wire.kalium.logic.data.message.UnreadEventType
+import com.wire.kalium.logic.data.mls.MLSPublicKeys
import com.wire.kalium.logic.data.user.AvailabilityStatusMapper
import com.wire.kalium.logic.data.user.BotService
import com.wire.kalium.logic.data.user.Connection
@@ -41,14 +43,18 @@ import com.wire.kalium.network.api.authenticated.conversation.ConvTeamInfo
import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse
import com.wire.kalium.network.api.authenticated.conversation.CreateConversationRequest
import com.wire.kalium.network.api.authenticated.conversation.ReceiptMode
+import com.wire.kalium.network.api.authenticated.serverpublickey.MLSPublicKeysDTO
import com.wire.kalium.network.api.model.ConversationAccessDTO
import com.wire.kalium.network.api.model.ConversationAccessRoleDTO
+import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity
import com.wire.kalium.persistence.dao.conversation.ConversationEntity
import com.wire.kalium.persistence.dao.conversation.ConversationEntity.GroupState
import com.wire.kalium.persistence.dao.conversation.ConversationEntity.Protocol
import com.wire.kalium.persistence.dao.conversation.ConversationEntity.ProtocolInfo
+import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity
import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity
import com.wire.kalium.persistence.dao.conversation.ProposalTimerEntity
+import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity
import com.wire.kalium.persistence.util.requireField
import com.wire.kalium.util.DateTimeUtil
import com.wire.kalium.util.time.UNIX_FIRST_DATE
@@ -59,13 +65,11 @@ import kotlin.time.toDuration
interface ConversationMapper {
fun fromApiModelToDaoModel(apiModel: ConversationResponse, mlsGroupState: GroupState?, selfUserTeamId: TeamId?): ConversationEntity
+ fun fromApiModel(mlsPublicKeysDTO: MLSPublicKeysDTO?): MLSPublicKeys?
+ fun fromDaoModel(daoModel: ConversationViewEntity): Conversation
fun fromDaoModel(daoModel: ConversationEntity): Conversation
- fun fromDaoModelToDetails(
- daoModel: ConversationViewEntity,
- lastMessage: MessagePreview?,
- unreadEventCount: UnreadEventCount?
- ): ConversationDetails
-
+ fun fromDaoModelToDetails(daoModel: ConversationViewEntity): ConversationDetails
+ fun fromDaoModelToDetailsWithEvents(daoModel: ConversationDetailsWithEventsEntity): ConversationDetailsWithEvents
fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer
fun toDAOAccess(accessList: Set): List
fun toDAOAccessRole(accessRoleList: Set): List
@@ -84,6 +88,11 @@ interface ConversationMapper {
fun legalHoldStatusFromEntity(legalHoldStatus: ConversationEntity.LegalHoldStatus): Conversation.LegalHoldStatus
fun fromConversationEntityType(type: ConversationEntity.Type): Conversation.Type
+
+ fun fromModelToDAOAccess(accessList: Set): List
+ fun fromModelToDAOAccessRole(accessRoleList: Set): List
+ fun fromApiModelToAccessModel(accessList: Set): Set
+ fun fromApiModelToAccessRoleModel(accessRoleList: Set): Set
}
@Suppress("TooManyFunctions", "LongParameterList")
@@ -96,7 +105,8 @@ internal class ConversationMapperImpl(
private val domainUserTypeMapper: DomainUserTypeMapper,
private val connectionStatusMapper: ConnectionStatusMapper,
private val conversationRoleMapper: ConversationRoleMapper,
- private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper()
+ private val messageMapper: MessageMapper,
+ private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper(),
) : ConversationMapper {
override fun fromApiModelToDaoModel(
@@ -117,7 +127,8 @@ internal class ConversationMapperImpl(
lastNotificationDate = null,
lastModifiedDate = apiModel.lastEventTime.toInstant(),
access = apiModel.access.map { it.toDAO() },
- accessRole = apiModel.accessRole.map { it.toDAO() },
+ accessRole = (apiModel.accessRole ?: ConversationAccessRoleDTO.DEFAULT_VALUE_WHEN_NULL)
+ .map { it.toDAO() },
receiptMode = receiptModeMapper.fromApiToDaoModel(apiModel.receiptMode),
messageTimer = apiModel.messageTimer,
userMessageTimer = null, // user picked self deletion timer is only persisted locally
@@ -158,6 +169,41 @@ internal class ConversationMapperImpl(
)
}
+ override fun fromApiModel(mlsPublicKeysDTO: MLSPublicKeysDTO?) = mlsPublicKeysDTO?.let {
+ MLSPublicKeys(
+ removal = mlsPublicKeysDTO.removal
+ )
+ }
+
+ override fun fromDaoModel(daoModel: ConversationViewEntity): Conversation = with(daoModel) {
+ val lastReadDateEntity = if (type == ConversationEntity.Type.CONNECTION_PENDING) Instant.UNIX_FIRST_DATE
+ else lastReadDate
+
+ Conversation(
+ id = id.toModel(),
+ name = name,
+ type = type.fromDaoModelToType(),
+ teamId = teamId?.let { TeamId(it) },
+ protocol = protocolInfoMapper.fromEntity(protocolInfo),
+ mutedStatus = conversationStatusMapper.fromMutedStatusDaoModel(mutedStatus),
+ removedBy = removedBy?.let { conversationStatusMapper.fromRemovedByToLogicModel(it) },
+ lastNotificationDate = lastNotificationDate,
+ lastModifiedDate = lastModifiedDate,
+ lastReadDate = lastReadDateEntity,
+ access = accessList.map { it.toDAO() },
+ accessRole = accessRoleList.map { it.toDAO() },
+ creatorId = creatorId,
+ receiptMode = receiptModeMapper.fromEntityToModel(receiptMode),
+ messageTimer = messageTimer?.toDuration(DurationUnit.MILLISECONDS),
+ userMessageTimer = userMessageTimer?.toDuration(DurationUnit.MILLISECONDS),
+ archived = archived,
+ archivedDateTime = archivedDateTime,
+ mlsVerificationStatus = verificationStatusFromEntity(mlsVerificationStatus),
+ proteusVerificationStatus = verificationStatusFromEntity(proteusVerificationStatus),
+ legalHoldStatus = legalHoldStatusFromEntity(legalHoldStatus)
+ )
+ }
+
override fun fromDaoModel(daoModel: ConversationEntity): Conversation = with(daoModel) {
val lastReadDateEntity = if (type == ConversationEntity.Type.CONNECTION_PENDING) Instant.UNIX_FIRST_DATE
else lastReadDate
@@ -187,11 +233,7 @@ internal class ConversationMapperImpl(
}
@Suppress("ComplexMethod", "LongMethod")
- override fun fromDaoModelToDetails(
- daoModel: ConversationViewEntity,
- lastMessage: MessagePreview?,
- unreadEventCount: UnreadEventCount?
- ): ConversationDetails =
+ override fun fromDaoModelToDetails(daoModel: ConversationViewEntity): ConversationDetails =
with(daoModel) {
when (type) {
ConversationEntity.Type.SELF -> {
@@ -204,7 +246,7 @@ internal class ConversationMapperImpl(
otherUser = OtherUser(
id = otherUserId.requireField("otherUserID in OneOnOne").toModel(),
name = name,
- accentId = 0,
+ accentId = accentId ?: 0,
userType = domainUserTypeMapper.fromUserTypeEntity(userType),
availabilityStatus = userAvailabilityStatusMapper.fromDaoAvailabilityStatusToModel(userAvailabilityStatus),
deleted = userDeleted ?: false,
@@ -221,8 +263,7 @@ internal class ConversationMapperImpl(
activeOneOnOneConversationId = userActiveOneOnOneConversationId?.toModel()
),
userType = domainUserTypeMapper.fromUserTypeEntity(userType),
- unreadEventCount = unreadEventCount ?: mapOf(),
- lastMessage = lastMessage
+ isFavorite = isFavorite
)
}
@@ -230,11 +271,9 @@ internal class ConversationMapperImpl(
ConversationDetails.Group(
conversation = fromConversationViewToEntity(daoModel),
hasOngoingCall = callStatus != null, // todo: we can do better!
- unreadEventCount = unreadEventCount ?: mapOf(),
- lastMessage = lastMessage,
isSelfUserMember = isMember,
- isSelfUserCreator = isCreator == 1L,
- selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }
+ selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) },
+ isFavorite = isFavorite
)
}
@@ -280,6 +319,27 @@ internal class ConversationMapperImpl(
}
}
+ override fun fromDaoModelToDetailsWithEvents(daoModel: ConversationDetailsWithEventsEntity): ConversationDetailsWithEvents =
+ ConversationDetailsWithEvents(
+ conversationDetails = fromDaoModelToDetails(daoModel.conversationViewEntity),
+ unreadEventCount = daoModel.unreadEvents.unreadEvents.mapKeys {
+ when (it.key) {
+ UnreadEventTypeEntity.KNOCK -> UnreadEventType.KNOCK
+ UnreadEventTypeEntity.MISSED_CALL -> UnreadEventType.MISSED_CALL
+ UnreadEventTypeEntity.MENTION -> UnreadEventType.MENTION
+ UnreadEventTypeEntity.REPLY -> UnreadEventType.REPLY
+ UnreadEventTypeEntity.MESSAGE -> UnreadEventType.MESSAGE
+ }
+ },
+ lastMessage = when {
+ daoModel.conversationViewEntity.archived -> null // no last message in archived conversations
+ daoModel.messageDraft != null -> messageMapper.fromDraftToMessagePreview(daoModel.messageDraft!!)
+ daoModel.lastMessage != null -> messageMapper.fromEntityToMessagePreview(daoModel.lastMessage!!)
+ else -> null
+ },
+ hasNewActivitiesToShow = daoModel.hasNewActivitiesToShow
+ )
+
override fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer =
ProposalTimer(idMapper.fromGroupIDEntity(daoModel.groupID), daoModel.firingDate)
@@ -459,6 +519,18 @@ internal class ConversationMapperImpl(
override fun fromConversationEntityType(type: ConversationEntity.Type): Conversation.Type {
return type.fromDaoModelToType()
}
+
+ override fun fromModelToDAOAccess(accessList: Set): List =
+ accessList.map { it.toDAO() }
+
+ override fun fromModelToDAOAccessRole(accessRoleList: Set): List =
+ accessRoleList.map { it.toDAO() }
+
+ override fun fromApiModelToAccessModel(accessList: Set): Set =
+ accessList.map { it.toModel() }.toSet()
+
+ override fun fromApiModelToAccessRoleModel(accessRoleList: Set): Set =
+ accessRoleList.map { it.toModel() }.toSet()
}
internal fun ConversationResponse.toConversationType(selfUserTeamId: TeamId?): ConversationEntity.Type {
@@ -551,6 +623,22 @@ private fun Conversation.Access.toDAO(): ConversationEntity.Access = when (this)
Conversation.Access.CODE -> ConversationEntity.Access.CODE
}
+private fun ConversationAccessDTO.toModel(): Conversation.Access = when (this) {
+ ConversationAccessDTO.PRIVATE -> Conversation.Access.PRIVATE
+ ConversationAccessDTO.CODE -> Conversation.Access.CODE
+ ConversationAccessDTO.INVITE -> Conversation.Access.INVITE
+ ConversationAccessDTO.SELF_INVITE -> Conversation.Access.SELF_INVITE
+ ConversationAccessDTO.LINK -> Conversation.Access.LINK
+}
+
+private fun ConversationAccessRoleDTO.toModel(): Conversation.AccessRole = when (this) {
+ ConversationAccessRoleDTO.TEAM_MEMBER -> Conversation.AccessRole.TEAM_MEMBER
+ ConversationAccessRoleDTO.NON_TEAM_MEMBER -> Conversation.AccessRole.NON_TEAM_MEMBER
+ ConversationAccessRoleDTO.GUEST -> Conversation.AccessRole.GUEST
+ ConversationAccessRoleDTO.SERVICE -> Conversation.AccessRole.SERVICE
+ ConversationAccessRoleDTO.EXTERNAL -> Conversation.AccessRole.EXTERNAL
+}
+
internal fun Conversation.Protocol.toApi(): ConvProtocol = when (this) {
Conversation.Protocol.PROTEUS -> ConvProtocol.PROTEUS
Conversation.Protocol.MIXED -> ConvProtocol.MIXED
@@ -586,3 +674,17 @@ internal fun ConversationEntity.VerificationStatus.toModel(): Conversation.Verif
ConversationEntity.VerificationStatus.NOT_VERIFIED -> Conversation.VerificationStatus.NOT_VERIFIED
ConversationEntity.VerificationStatus.DEGRADED -> Conversation.VerificationStatus.DEGRADED
}
+
+internal fun ConversationFilter.toDao(): ConversationFilterEntity = when (this) {
+ ConversationFilter.ALL -> ConversationFilterEntity.ALL
+ ConversationFilter.FAVORITES -> ConversationFilterEntity.FAVORITES
+ ConversationFilter.GROUPS -> ConversationFilterEntity.GROUPS
+ ConversationFilter.ONE_ON_ONE -> ConversationFilterEntity.ONE_ON_ONE
+}
+
+internal fun ConversationFilterEntity.toModel(): ConversationFilter = when (this) {
+ ConversationFilterEntity.ALL -> ConversationFilter.ALL
+ ConversationFilterEntity.FAVORITES -> ConversationFilter.FAVORITES
+ ConversationFilterEntity.GROUPS -> ConversationFilter.GROUPS
+ ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.ONE_ON_ONE
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt
index 9bbf027020b..f199fe862f7 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt
@@ -25,6 +25,7 @@ import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.client.MLSClientProvider
import com.wire.kalium.logic.data.conversation.Conversation.ProtocolInfo.MLSCapable.GroupState
import com.wire.kalium.logic.data.conversation.mls.EpochChangesData
+import com.wire.kalium.logic.data.conversation.mls.NameAndHandle
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.GroupID
import com.wire.kalium.logic.data.id.IdMapper
@@ -36,9 +37,7 @@ import com.wire.kalium.logic.data.id.toApi
import com.wire.kalium.logic.data.id.toCrypto
import com.wire.kalium.logic.data.id.toDao
import com.wire.kalium.logic.data.id.toModel
-import com.wire.kalium.logic.data.message.MessageMapper
import com.wire.kalium.logic.data.message.SelfDeletionTimer
-import com.wire.kalium.logic.data.message.UnreadEventType
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.di.MapperProvider
import com.wire.kalium.logic.functional.Either
@@ -57,8 +56,6 @@ import com.wire.kalium.logic.kaliumLogger
import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.logic.wrapMLSRequest
import com.wire.kalium.logic.wrapStorageRequest
-import com.wire.kalium.network.api.base.authenticated.client.ClientApi
-import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi
import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO
import com.wire.kalium.network.api.authenticated.conversation.ConversationRenameResponse
import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse
@@ -67,28 +64,32 @@ import com.wire.kalium.network.api.authenticated.conversation.UpdateConversation
import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationReceiptModeResponse
import com.wire.kalium.network.api.authenticated.conversation.model.ConversationMemberRoleDTO
import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO
+import com.wire.kalium.network.api.base.authenticated.client.ClientApi
+import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi
import com.wire.kalium.persistence.dao.QualifiedIDEntity
import com.wire.kalium.persistence.dao.client.ClientDAO
import com.wire.kalium.persistence.dao.conversation.ConversationDAO
+import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity
import com.wire.kalium.persistence.dao.conversation.ConversationEntity
import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO
import com.wire.kalium.persistence.dao.member.MemberDAO
import com.wire.kalium.persistence.dao.message.MessageDAO
import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO
-import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity
+import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity
import com.wire.kalium.util.DelicateKaliumApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Instant
@Suppress("TooManyFunctions")
interface ConversationRepository {
+ val extensions: ConversationRepositoryExtensions
+
// region Get/Observe by id
suspend fun observeConversationById(conversationId: ConversationId): Flow>
@@ -129,7 +130,16 @@ interface ConversationRepository {
suspend fun getConversationList(): Either>>
suspend fun observeConversationList(): Flow>
- suspend fun observeConversationListDetails(fromArchive: Boolean): Flow>
+ suspend fun observeConversationListDetails(
+ fromArchive: Boolean,
+ conversationFilter: ConversationFilter = ConversationFilter.ALL
+ ): Flow>
+
+ suspend fun observeConversationListDetailsWithEvents(
+ fromArchive: Boolean = false,
+ conversationFilter: ConversationFilter = ConversationFilter.ALL
+ ): Flow>
+
suspend fun getConversationIds(
type: Conversation.Type,
protocol: Conversation.Protocol,
@@ -303,6 +313,7 @@ interface ConversationRepository {
suspend fun observeLegalHoldStatusChangeNotified(conversationId: ConversationId): Flow>
suspend fun getGroupStatusMembersNamesAndHandles(groupID: GroupID): Either
+ suspend fun selectMembersNameAndHandle(conversationId: ConversationId): Either>
}
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
@@ -314,19 +325,20 @@ internal class ConversationDataSource internal constructor(
private val memberDAO: MemberDAO,
private val conversationApi: ConversationApi,
private val messageDAO: MessageDAO,
+ private val messageDraftDAO: MessageDraftDAO,
private val clientDAO: ClientDAO,
private val clientApi: ClientApi,
private val conversationMetaDataDAO: ConversationMetaDataDAO,
- private val messageDraftDAO: MessageDraftDAO,
private val idMapper: IdMapper = MapperProvider.idMapper(),
private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId),
private val memberMapper: MemberMapper = MapperProvider.memberMapper(),
private val conversationStatusMapper: ConversationStatusMapper = MapperProvider.conversationStatusMapper(),
private val conversationRoleMapper: ConversationRoleMapper = MapperProvider.conversationRoleMapper(),
private val protocolInfoMapper: ProtocolInfoMapper = MapperProvider.protocolInfoMapper(),
- private val messageMapper: MessageMapper = MapperProvider.messageMapper(selfUserId),
private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper()
) : ConversationRepository {
+ override val extensions: ConversationRepositoryExtensions =
+ ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper)
// region Get/Observe by id
@@ -347,14 +359,14 @@ internal class ConversationDataSource internal constructor(
conversationMapper.fromConversationEntityType(it)
}
}
+
override suspend fun observeConversationDetailsById(conversationID: ConversationId): Flow> =
conversationDAO.observeConversationDetailsById(conversationID.toDao())
.wrapStorageRequest()
- // TODO we don't need last message and unread count here, we should discuss to divide model for list and for details
.map { eitherConversationView ->
eitherConversationView.flatMap {
try {
- Either.Right(conversationMapper.fromDaoModelToDetails(it, null, mapOf()))
+ Either.Right(conversationMapper.fromDaoModelToDetails(it))
} catch (error: IllegalArgumentException) {
kaliumLogger.e("require field in conversation Details", error)
Either.Left(StorageFailure.DataNotFound)
@@ -512,30 +524,36 @@ internal class ConversationDataSource internal constructor(
return conversationDAO.getAllConversations().map { it.map(conversationMapper::fromDaoModel) }
}
- override suspend fun observeConversationListDetails(fromArchive: Boolean): Flow> =
+ override suspend fun observeConversationListDetails(
+ fromArchive: Boolean,
+ conversationFilter: ConversationFilter
+ ): Flow> =
+ conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()).map { conversationViewEntityList ->
+ conversationViewEntityList.map { conversationViewEntity -> conversationMapper.fromDaoModelToDetails(conversationViewEntity) }
+ }
+
+ override suspend fun observeConversationListDetailsWithEvents(
+ fromArchive: Boolean,
+ conversationFilter: ConversationFilter
+ ): Flow> =
combine(
- conversationDAO.getAllConversationDetails(fromArchive),
+ conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()),
if (fromArchive) flowOf(listOf()) else messageDAO.observeLastMessages(),
messageDAO.observeConversationsUnreadEvents(),
messageDraftDAO.observeMessageDrafts()
) { conversationList, lastMessageList, unreadEvents, drafts ->
val lastMessageMap = lastMessageList.associateBy { it.conversationId }
val messageDraftMap = drafts.filter { it.text.isNotBlank() }.associateBy { it.conversationId }
+ val unreadEventsMap = unreadEvents.associateBy { it.conversationId }
conversationList.map { conversation ->
- conversationMapper.fromDaoModelToDetails(
- conversation,
- lastMessage = messageDraftMap[conversation.id]?.let { messageMapper.fromDraftToMessagePreview(it) }
- ?: lastMessageMap[conversation.id]?.let { messageMapper.fromEntityToMessagePreview(it) },
- unreadEventCount = unreadEvents.firstOrNull { it.conversationId == conversation.id }?.unreadEvents?.mapKeys {
- when (it.key) {
- UnreadEventTypeEntity.KNOCK -> UnreadEventType.KNOCK
- UnreadEventTypeEntity.MISSED_CALL -> UnreadEventType.MISSED_CALL
- UnreadEventTypeEntity.MENTION -> UnreadEventType.MENTION
- UnreadEventTypeEntity.REPLY -> UnreadEventType.REPLY
- UnreadEventTypeEntity.MESSAGE -> UnreadEventType.MESSAGE
- }
- }
+ conversationMapper.fromDaoModelToDetailsWithEvents(
+ ConversationDetailsWithEventsEntity(
+ conversationViewEntity = conversation,
+ lastMessage = lastMessageMap[conversation.id],
+ messageDraft = messageDraftMap[conversation.id],
+ unreadEvents = unreadEventsMap[conversation.id] ?: ConversationUnreadEventEntity(conversation.id, mapOf()),
+ )
)
}
}
@@ -544,6 +562,7 @@ internal class ConversationDataSource internal constructor(
wrapApiRequest {
conversationApi.fetchMlsOneToOneConversation(userId.toApi())
}.map { conversationResponse ->
+ // question: do we need to do this? since it's one on one!
addOtherMemberIfMissing(conversationResponse, userId)
}.flatMap { conversationResponse ->
val selfUserTeamId = selfTeamIdProvider().getOrNull()
@@ -552,7 +571,9 @@ internal class ConversationDataSource internal constructor(
selfUserTeamId = selfUserTeamId
).map { conversationResponse }
}.flatMap { response ->
- this.getConversationById(response.id.toModel())
+ this.getConversationById(response.id.toModel()).map {
+ it.copy(mlsPublicKeys = conversationMapper.fromApiModel(response.publicKeys))
+ }
}
private fun addOtherMemberIfMissing(
@@ -988,8 +1009,8 @@ internal class ConversationDataSource internal constructor(
}
override suspend fun getConversationDetailsByMLSGroupId(mlsGroupId: GroupID): Either =
- wrapStorageRequest { conversationDAO.getConversationByGroupID(mlsGroupId.value) }
- .map { conversationMapper.fromDaoModelToDetails(it, null, mapOf()) }
+ wrapStorageRequest { conversationDAO.getConversationDetailsByGroupID(mlsGroupId.value) }
+ .map { conversationMapper.fromDaoModelToDetails(it) }
override suspend fun observeUnreadArchivedConversationsCount(): Flow =
conversationDAO.observeUnreadArchivedConversationsCount()
@@ -1102,6 +1123,13 @@ internal class ConversationDataSource internal constructor(
conversationDAO.selectGroupStatusMembersNamesAndHandles(groupID.value)
}.map { EpochChangesData.fromEntity(it) }
+ override suspend fun selectMembersNameAndHandle(conversationId: ConversationId): Either> =
+ wrapStorageRequest {
+ memberDAO.selectMembersNameAndHandle(conversationId.toDao())
+ .mapValues { NameAndHandle.fromEntity(it.value) }
+ .mapKeys { it.key.toModel() }
+ }
+
companion object {
const val DEFAULT_MEMBER_ROLE = "wire_member"
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt
new file mode 100644
index 00000000000..aec18932a97
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt
@@ -0,0 +1,77 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.conversation
+
+import app.cash.paging.PagingConfig
+import app.cash.paging.PagingData
+import app.cash.paging.map
+import com.wire.kalium.persistence.dao.conversation.ConversationDAO
+import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity
+import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig
+import com.wire.kalium.persistence.dao.message.KaliumPager
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+interface ConversationRepositoryExtensions {
+ suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery(
+ queryConfig: ConversationQueryConfig,
+ pagingConfig: PagingConfig,
+ startingOffset: Long,
+ ): Flow>
+}
+
+class ConversationRepositoryExtensionsImpl internal constructor(
+ private val conversationDAO: ConversationDAO,
+ private val conversationMapper: ConversationMapper
+) : ConversationRepositoryExtensions {
+ override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery(
+ queryConfig: ConversationQueryConfig,
+ pagingConfig: PagingConfig,
+ startingOffset: Long
+ ): Flow> {
+ val pager: KaliumPager = with(queryConfig) {
+ conversationDAO.platformExtensions.getPagerForConversationDetailsWithEventsSearch(
+ queryConfig = QueryConfig(
+ searchQuery = searchQuery,
+ fromArchive = fromArchive,
+ onlyInteractionEnabled = onlyInteractionEnabled,
+ newActivitiesOnTop = newActivitiesOnTop,
+ conversationFilter = conversationFilter.toDao()
+ ),
+ pagingConfig = pagingConfig
+ )
+ }
+
+ return pager.pagingDataFlow.map { pagingData ->
+ pagingData
+ .map { conversationDetailsWithEventsEntity ->
+ conversationMapper.fromDaoModelToDetailsWithEvents(
+ conversationDetailsWithEventsEntity
+ )
+ }
+ }
+ }
+}
+
+data class ConversationQueryConfig(
+ val searchQuery: String = "",
+ val fromArchive: Boolean = false,
+ val onlyInteractionEnabled: Boolean = false,
+ val newActivitiesOnTop: Boolean = false,
+ val conversationFilter: ConversationFilter = ConversationFilter.ALL,
+)
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt
index e94a5864fa1..942c55a22b2 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt
@@ -25,6 +25,7 @@ import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.client.ClientRepository
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.toApi
+import com.wire.kalium.logic.data.mls.MLSPublicKeys
import com.wire.kalium.logic.featureFlags.FeatureSupport
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMap
@@ -51,7 +52,7 @@ import kotlinx.coroutines.withContext
* but has not yet joined the corresponding MLS group.
*/
internal interface JoinExistingMLSConversationUseCase {
- suspend operator fun invoke(conversationId: ConversationId): Either
+ suspend operator fun invoke(conversationId: ConversationId, mlsPublicKeys: MLSPublicKeys? = null): Either
}
@Suppress("LongParameterList")
@@ -65,7 +66,7 @@ internal class JoinExistingMLSConversationUseCaseImpl(
) : JoinExistingMLSConversationUseCase {
private val dispatcher = kaliumDispatcher.io
- override suspend operator fun invoke(conversationId: ConversationId): Either =
+ override suspend operator fun invoke(conversationId: ConversationId, mlsPublicKeys: MLSPublicKeys?): Either =
if (!featureSupport.isMLSSupported ||
!clientRepository.hasRegisteredMLSClient().getOrElse(false)
) {
@@ -76,15 +77,16 @@ internal class JoinExistingMLSConversationUseCaseImpl(
Either.Left(StorageFailure.DataNotFound)
}, { conversation ->
withContext(dispatcher) {
- joinOrEstablishMLSGroupAndRetry(conversation)
+ joinOrEstablishMLSGroupAndRetry(conversation, mlsPublicKeys)
}
})
}
private suspend fun joinOrEstablishMLSGroupAndRetry(
- conversation: Conversation
+ conversation: Conversation,
+ mlsPublicKeys: MLSPublicKeys?
): Either =
- joinOrEstablishMLSGroup(conversation)
+ joinOrEstablishMLSGroup(conversation, mlsPublicKeys)
.flatMapLeft { failure ->
if (failure is NetworkFailure.ServerMiscommunication && failure.kaliumException is KaliumException.InvalidRequestError) {
if (failure.kaliumException.isMlsStaleMessage()) {
@@ -101,13 +103,15 @@ internal class JoinExistingMLSConversationUseCaseImpl(
// Re-fetch current epoch and try again
if (conversation.type == Conversation.Type.ONE_ON_ONE) {
conversationRepository.getConversationMembers(conversation.id).flatMap {
- conversationRepository.fetchMlsOneToOneConversation(it.first())
+ conversationRepository.fetchMlsOneToOneConversation(it.first()).map {
+ it.mlsPublicKeys
+ }
}
} else {
conversationRepository.fetchConversation(conversation.id)
}.flatMap {
conversationRepository.getConversationById(conversation.id).flatMap { conversation ->
- joinOrEstablishMLSGroup(conversation)
+ joinOrEstablishMLSGroup(conversation, null)
}
}
} else if (failure.kaliumException.isMlsMissingGroupInfo()) {
@@ -122,7 +126,7 @@ internal class JoinExistingMLSConversationUseCaseImpl(
}
@Suppress("LongMethod")
- private suspend fun joinOrEstablishMLSGroup(conversation: Conversation): Either {
+ private suspend fun joinOrEstablishMLSGroup(conversation: Conversation, publicKeys: MLSPublicKeys?): Either {
val protocol = conversation.protocol
val type = conversation.type
return when {
@@ -202,7 +206,8 @@ internal class JoinExistingMLSConversationUseCaseImpl(
conversationRepository.getConversationMembers(conversation.id).flatMap { members ->
mlsConversationRepository.establishMLSGroup(
protocol.groupId,
- members
+ members,
+ publicKeys
)
}.onSuccess {
kaliumLogger.logStructuredJson(
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt
index 6c90376d6ae..ee7f3428c55 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt
@@ -50,6 +50,7 @@ internal interface JoinSubconversationUseCase {
suspend operator fun invoke(conversationId: ConversationId, subconversationId: SubconversationId): Either
}
+// TODO(refactor): usecase should not access API class directly, use SubconversationRepository instead
internal class JoinSubconversationUseCaseImpl(
private val conversationApi: ConversationApi,
private val mlsConversationRepository: MLSConversationRepository,
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt
index 058a4736ce7..c6e8c608c27 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt
@@ -46,9 +46,11 @@ import com.wire.kalium.logic.data.keypackage.KeyPackageLimitsProvider
import com.wire.kalium.logic.data.keypackage.KeyPackageRepository
import com.wire.kalium.logic.data.mls.CipherSuite
import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository
+import com.wire.kalium.logic.data.mlspublickeys.getRemovalKey
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.di.MapperProvider
import com.wire.kalium.logic.data.e2ei.RevocationListChecker
+import com.wire.kalium.logic.data.mls.MLSPublicKeys
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.flatMapLeft
@@ -123,6 +125,7 @@ interface MLSConversationRepository {
suspend fun establishMLSGroup(
groupID: GroupID,
members: List,
+ publicKeys: MLSPublicKeys? = null,
allowSkippingUsersWithoutKeyPackages: Boolean = false
): Either
@@ -554,16 +557,18 @@ internal class MLSConversationDataSource(
override suspend fun establishMLSGroup(
groupID: GroupID,
members: List,
- allowSkippingUsersWithoutKeyPackages: Boolean,
+ publicKeys: MLSPublicKeys?,
+ allowSkippingUsersWithoutKeyPackages: Boolean
): Either = withContext(serialDispatcher) {
- mlsClientProvider.getMLSClient().flatMap {
- mlsPublicKeysRepository.getKeyForCipherSuite(
- CipherSuite.fromTag(it.getDefaultCipherSuite())
- ).flatMap { key ->
+ mlsClientProvider.getMLSClient().flatMap { mlsClient ->
+ val cipherSuite = CipherSuite.fromTag(mlsClient.getDefaultCipherSuite())
+ val keys = publicKeys?.getRemovalKey(cipherSuite) ?: mlsPublicKeysRepository.getKeyForCipherSuite(cipherSuite)
+
+ keys.flatMap { externalSenders ->
establishMLSGroup(
groupID = groupID,
members = members,
- externalSenders = key,
+ externalSenders = externalSenders,
allowPartialMemberList = allowSkippingUsersWithoutKeyPackages
)
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt
index f2a1a6e8a9b..d6281310b22 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt
@@ -17,34 +17,81 @@
*/
package com.wire.kalium.logic.data.conversation
+import com.wire.kalium.logic.CoreFailure
+import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.GroupID
import com.wire.kalium.logic.data.id.SubconversationId
+import com.wire.kalium.logic.data.id.toApi
+import com.wire.kalium.logic.functional.Either
+import com.wire.kalium.logic.functional.onFailure
+import com.wire.kalium.logic.functional.onSuccess
+import com.wire.kalium.logic.kaliumLogger
+import com.wire.kalium.logic.wrapApiRequest
+import com.wire.kalium.network.api.authenticated.conversation.SubconversationDeleteRequest
+import com.wire.kalium.network.api.authenticated.conversation.SubconversationResponse
+import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi
import io.ktor.util.collections.ConcurrentMap
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface SubconversationRepository {
- suspend fun insertSubconversation(conversationId: ConversationId, subconversationId: SubconversationId, groupId: GroupID)
- suspend fun getSubconversationInfo(conversationId: ConversationId, subconversationId: SubconversationId): GroupID?
- suspend fun deleteSubconversation(conversationId: ConversationId, subconversationId: SubconversationId)
+ suspend fun insertSubconversation(
+ conversationId: ConversationId,
+ subconversationId: SubconversationId,
+ groupId: GroupID
+ )
+
+ suspend fun getSubconversationInfo(
+ conversationId: ConversationId,
+ subconversationId: SubconversationId
+ ): GroupID?
+
+ suspend fun deleteSubconversation(
+ conversationId: ConversationId,
+ subconversationId: SubconversationId
+ )
+
suspend fun containsSubconversation(groupId: GroupID): Boolean
+ suspend fun deleteRemoteSubConversation(
+ conversationId: ConversationId,
+ subConversationId: SubconversationId,
+ subConversationDeleteRequest: SubconversationDeleteRequest
+ ): Either
+ suspend fun fetchRemoteSubConversationGroupInfo(
+ conversationId: ConversationId,
+ subConversationId: SubconversationId
+ ): Either
+
+ suspend fun fetchRemoteSubConversationDetails(
+ conversationId: ConversationId,
+ subConversationId: SubconversationId
+ ): Either
}
-class SubconversationRepositoryImpl : SubconversationRepository {
+class SubconversationRepositoryImpl(
+ private val conversationApi: ConversationApi
+) : SubconversationRepository {
private val mutex = Mutex()
private val subconversations = ConcurrentMap, GroupID>()
- override suspend fun insertSubconversation(conversationId: ConversationId, subconversationId: SubconversationId, groupId: GroupID) {
+ override suspend fun insertSubconversation(
+ conversationId: ConversationId,
+ subconversationId: SubconversationId,
+ groupId: GroupID
+ ) {
mutex.withLock {
subconversations[Pair(conversationId, subconversationId)] = groupId
}
}
- override suspend fun getSubconversationInfo(conversationId: ConversationId, subconversationId: SubconversationId): GroupID? {
+ override suspend fun getSubconversationInfo(
+ conversationId: ConversationId,
+ subconversationId: SubconversationId
+ ): GroupID? {
mutex.withLock {
return subconversations[Pair(conversationId, subconversationId)]
}
@@ -56,10 +103,50 @@ class SubconversationRepositoryImpl : SubconversationRepository {
}
}
- override suspend fun deleteSubconversation(conversationId: ConversationId, subconversationId: SubconversationId) {
+ override suspend fun deleteSubconversation(
+ conversationId: ConversationId,
+ subconversationId: SubconversationId
+ ) {
mutex.withLock {
subconversations.remove(Pair(conversationId, subconversationId))
}
}
+ override suspend fun deleteRemoteSubConversation(
+ conversationId: ConversationId,
+ subConversationId: SubconversationId,
+ subConversationDeleteRequest: SubconversationDeleteRequest
+ ): Either =
+ wrapApiRequest {
+ conversationApi.deleteSubconversation(
+ conversationId.toApi(),
+ subConversationId.toApi(),
+ subConversationDeleteRequest
+ )
+ }.onSuccess {
+ kaliumLogger.i("Subconversation deleted successfully")
+ }.onFailure {
+ kaliumLogger.i("Failed to delete subconversation")
+ }
+
+ override suspend fun fetchRemoteSubConversationGroupInfo(
+ conversationId: ConversationId,
+ subConversationId: SubconversationId
+ ): Either = wrapApiRequest {
+ conversationApi.fetchSubconversationGroupInfo(
+ conversationId.toApi(),
+ subConversationId.toApi()
+ )
+ }
+
+ // TODO: Replace SubconversationResponse with a domain model
+ override suspend fun fetchRemoteSubConversationDetails(
+ conversationId: ConversationId,
+ subConversationId: SubconversationId
+ ): Either = wrapApiRequest {
+ conversationApi.fetchSubconversationDetails(
+ conversationId.toApi(),
+ subConversationId.toApi()
+ )
+ }
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt
new file mode 100644
index 00000000000..fc20894926b
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt
@@ -0,0 +1,86 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.conversation.folders
+
+import com.wire.kalium.logic.data.conversation.ConversationFolder
+import com.wire.kalium.logic.data.conversation.FolderType
+import com.wire.kalium.logic.data.conversation.FolderWithConversations
+import com.wire.kalium.logic.data.id.QualifiedID
+import com.wire.kalium.logic.data.id.toApi
+import com.wire.kalium.logic.data.id.toDao
+import com.wire.kalium.logic.data.id.toModel
+import com.wire.kalium.network.api.authenticated.properties.LabelDTO
+import com.wire.kalium.network.api.authenticated.properties.LabelTypeDTO
+import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity
+import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity
+import com.wire.kalium.persistence.dao.conversation.folder.FolderWithConversationsEntity
+
+fun LabelDTO.toFolder(selfDomain: String) = FolderWithConversations(
+ conversationIdList = qualifiedConversations?.map { it.toModel() } ?: conversations.map { QualifiedID(it, selfDomain) },
+ id = id,
+ name = name,
+ type = type.toFolderType()
+)
+
+fun FolderWithConversations.toLabel() = LabelDTO(
+ id = id,
+ name = name,
+ qualifiedConversations = conversationIdList.map { it.toApi() },
+ conversations = conversationIdList.map { it.value },
+ type = type.toLabel()
+)
+
+fun LabelTypeDTO.toFolderType() = when (this) {
+ LabelTypeDTO.USER -> FolderType.USER
+ LabelTypeDTO.FAVORITE -> FolderType.FAVORITE
+}
+
+fun FolderType.toLabel() = when (this) {
+ FolderType.USER -> LabelTypeDTO.USER
+ FolderType.FAVORITE -> LabelTypeDTO.FAVORITE
+}
+
+fun ConversationFolderEntity.toModel() = ConversationFolder(
+ id = id,
+ name = name,
+ type = type.toModel()
+)
+
+fun FolderWithConversationsEntity.toModel() = FolderWithConversations(
+ id = id,
+ name = name,
+ type = type.toModel(),
+ conversationIdList = conversationIdList.map { it.toModel() }
+)
+
+fun FolderWithConversations.toDao() = FolderWithConversationsEntity(
+ id = id,
+ name = name,
+ type = type.toDao(),
+ conversationIdList = conversationIdList.map { it.toDao() }
+)
+
+fun FolderType.toDao() = when (this) {
+ FolderType.USER -> ConversationFolderTypeEntity.USER
+ FolderType.FAVORITE -> ConversationFolderTypeEntity.FAVORITE
+}
+
+fun ConversationFolderTypeEntity.toModel(): FolderType = when (this) {
+ ConversationFolderTypeEntity.USER -> FolderType.USER
+ ConversationFolderTypeEntity.FAVORITE -> FolderType.FAVORITE
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt
new file mode 100644
index 00000000000..70ad373bdfa
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt
@@ -0,0 +1,155 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.conversation.folders
+
+import com.benasher44.uuid.uuid4
+import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.CONVERSATIONS_FOLDERS
+import com.wire.kalium.logger.obfuscateId
+import com.wire.kalium.logic.CoreFailure
+import com.wire.kalium.logic.NetworkFailure
+import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents
+import com.wire.kalium.logic.data.conversation.ConversationFolder
+import com.wire.kalium.logic.data.conversation.ConversationMapper
+import com.wire.kalium.logic.data.conversation.FolderType
+import com.wire.kalium.logic.data.conversation.FolderWithConversations
+import com.wire.kalium.logic.data.id.QualifiedID
+import com.wire.kalium.logic.data.id.toDao
+import com.wire.kalium.logic.di.MapperProvider
+import com.wire.kalium.logic.functional.Either
+import com.wire.kalium.logic.functional.flatMap
+import com.wire.kalium.logic.functional.flatMapLeft
+import com.wire.kalium.logic.functional.map
+import com.wire.kalium.logic.functional.onFailure
+import com.wire.kalium.logic.functional.onSuccess
+import com.wire.kalium.logic.kaliumLogger
+import com.wire.kalium.logic.wrapApiRequest
+import com.wire.kalium.logic.wrapStorageRequest
+import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO
+import com.wire.kalium.network.api.authenticated.properties.PropertyKey
+import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi
+import com.wire.kalium.network.exceptions.KaliumException
+import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO
+import io.ktor.http.HttpStatusCode
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+internal interface ConversationFolderRepository {
+
+ suspend fun getFavoriteConversationFolder(): Either
+ suspend fun observeConversationsFromFolder(folderId: String): Flow>
+ suspend fun updateConversationFolders(folderWithConversations: List): Either
+ suspend fun fetchConversationFolders(): Either
+ suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either
+ suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either
+ suspend fun syncConversationFoldersFromLocal(): Either
+}
+
+internal class ConversationFolderDataSource internal constructor(
+ private val conversationFolderDAO: ConversationFolderDAO,
+ private val userPropertiesApi: PropertiesApi,
+ private val selfUserId: QualifiedID,
+ private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId)
+) : ConversationFolderRepository {
+
+ override suspend fun updateConversationFolders(folderWithConversations: List): Either =
+ wrapStorageRequest {
+ conversationFolderDAO.updateConversationFolders(folderWithConversations.map { it.toDao() })
+ }
+
+ override suspend fun getFavoriteConversationFolder(): Either = wrapStorageRequest {
+ conversationFolderDAO.getFavoriteConversationFolder().toModel()
+ }
+
+ override suspend fun observeConversationsFromFolder(folderId: String): Flow> =
+ conversationFolderDAO.observeConversationListFromFolder(folderId).map { conversationDetailsWithEventsEntityList ->
+ conversationDetailsWithEventsEntityList.map {
+ conversationMapper.fromDaoModelToDetailsWithEvents(it)
+ }
+ }
+
+ override suspend fun fetchConversationFolders(): Either = wrapApiRequest {
+ kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Fetching conversation folders")
+ userPropertiesApi.getLabels()
+ }
+ .flatMapLeft {
+ if (it is NetworkFailure.ServerMiscommunication
+ && it.kaliumException is KaliumException.InvalidRequestError
+ && it.kaliumException.errorResponse.code == HttpStatusCode.NotFound.value
+ ) {
+ kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("User has no labels, creating an empty list")
+ // If the user has no labels, we create an empty list and on next stage we will create a favorite label
+ Either.Right(LabelListResponseDTO(emptyList()))
+ } else {
+ Either.Left(it)
+ }
+ }
+ .onSuccess { labelsResponse ->
+ val folders = labelsResponse.labels.map { it.toFolder(selfUserId.domain) }.toMutableList()
+ val favoriteLabel = folders.firstOrNull { it.type == FolderType.FAVORITE }
+
+ if (favoriteLabel == null) {
+ kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Favorite label not found, creating a new one")
+ folders.add(
+ FolderWithConversations(
+ id = uuid4().toString(),
+ name = "", // name will be handled by localization
+ type = FolderType.FAVORITE,
+ conversationIdList = emptyList()
+ )
+ )
+ userPropertiesApi.setProperty(
+ PropertyKey.WIRE_LABELS,
+ labelsResponse.copy(labels = folders.map { it.toLabel() })
+ )
+ }
+ conversationFolderDAO.updateConversationFolders(folders.map { it.toDao() })
+ }
+ .onFailure {
+ kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).e("Error fetching conversation folders $it")
+ Either.Left(it)
+ }
+ .map { }
+
+ override suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either {
+ kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS)
+ .v("Adding conversation ${conversationId.toLogString()} to folder ${folderId.obfuscateId()}")
+ return wrapStorageRequest {
+ conversationFolderDAO.addConversationToFolder(conversationId.toDao(), folderId)
+ }
+ }
+
+ override suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either {
+ kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS)
+ .v("Removing conversation ${conversationId.toLogString()} from folder ${folderId.obfuscateId()}")
+ return wrapStorageRequest {
+ conversationFolderDAO.removeConversationFromFolder(conversationId.toDao(), folderId)
+ }
+ }
+
+ override suspend fun syncConversationFoldersFromLocal(): Either {
+ kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Syncing conversation folders from local")
+ return wrapStorageRequest { conversationFolderDAO.getFoldersWithConversations().map { it.toModel() } }
+ .flatMap {
+ wrapApiRequest {
+ userPropertiesApi.updateLabels(
+ LabelListResponseDTO(it.map { it.toLabel() })
+ )
+ }
+ }
+ }
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt
index c70c52611d8..a56b28876d6 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt
@@ -32,7 +32,7 @@ import com.wire.kalium.logic.kaliumLogger
/**
* Use case to check if the CRL is expired and if so, register CRL and update conversation statuses if there is a change.
*/
-interface RevocationListChecker {
+internal interface RevocationListChecker {
suspend fun check(url: String): Either
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt
index ea9fdebba3a..a7f3f2ca919 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt
@@ -23,10 +23,13 @@ import com.wire.kalium.logger.obfuscateDomain
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.data.client.Client
import com.wire.kalium.logic.data.conversation.ClientId
+import com.wire.kalium.logic.data.conversation.Conversation.Access
+import com.wire.kalium.logic.data.conversation.Conversation.AccessRole
import com.wire.kalium.logic.data.conversation.Conversation.Member
import com.wire.kalium.logic.data.conversation.Conversation.Protocol
import com.wire.kalium.logic.data.conversation.Conversation.ReceiptMode
import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode
+import com.wire.kalium.logic.data.conversation.FolderWithConversations
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.featureConfig.AppLockModel
import com.wire.kalium.logic.data.featureConfig.ClassifiedDomainsModel
@@ -55,7 +58,7 @@ import kotlinx.serialization.json.JsonNull
*/
data class EventEnvelope(
val event: Event,
- val deliveryInfo: EventDeliveryInfo,
+ val deliveryInfo: EventDeliveryInfo
) {
override fun toString(): String {
return super.toString()
@@ -124,7 +127,8 @@ sealed class Event(open val id: String) {
data class AccessUpdate(
override val id: String,
override val conversationId: ConversationId,
- val data: ConversationResponse,
+ val access: Set,
+ val accessRole: Set,
val qualifiedFrom: UserId,
) : Conversation(id, conversationId) {
@@ -387,14 +391,16 @@ sealed class Event(open val id: String) {
val uri: String?,
val isPasswordProtected: Boolean,
) : Conversation(id, conversationId) {
- override fun toLogMap(): Map = mapOf(typeKey to "Conversation.CodeUpdated")
+ override fun toLogMap(): Map =
+ mapOf(typeKey to "Conversation.CodeUpdated")
}
data class CodeDeleted(
override val id: String,
override val conversationId: ConversationId,
) : Conversation(id, conversationId) {
- override fun toLogMap(): Map = mapOf(typeKey to "Conversation.CodeDeleted")
+ override fun toLogMap(): Map =
+ mapOf(typeKey to "Conversation.CodeDeleted")
}
data class TypingIndicator(
@@ -705,6 +711,17 @@ sealed class Event(open val id: String) {
"value" to "$value"
)
}
+
+ data class FoldersUpdate(
+ override val id: String,
+ val folders: List,
+ ) : UserProperty(id) {
+ override fun toLogMap(): Map = mapOf(
+ typeKey to "User.UserProperty.FoldersUpdate",
+ idKey to id.obfuscateId(),
+ "folders" to folders.map { it.id.obfuscateId() }
+ )
+ }
}
data class Unknown(
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventGenerator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventGenerator.kt
new file mode 100644
index 00000000000..bb894034c2b
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventGenerator.kt
@@ -0,0 +1,127 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.data.event
+
+import com.benasher44.uuid.uuid4
+import com.wire.kalium.cryptography.CryptoClientId
+import com.wire.kalium.cryptography.CryptoSessionId
+import com.wire.kalium.cryptography.ProteusClient
+import com.wire.kalium.logic.data.conversation.Conversation
+import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.data.id.QualifiedClientID
+import com.wire.kalium.logic.data.id.toApi
+import com.wire.kalium.logic.data.id.toCrypto
+import com.wire.kalium.logic.data.message.MessageContent
+import com.wire.kalium.logic.data.message.PlainMessageBlob
+import com.wire.kalium.logic.data.message.ProtoContent
+import com.wire.kalium.logic.data.message.ProtoContentMapper
+import com.wire.kalium.logic.data.user.UserId
+import com.wire.kalium.logic.di.MapperProvider
+import com.wire.kalium.network.api.authenticated.notification.EventContentDTO
+import com.wire.kalium.network.api.authenticated.notification.EventResponse
+import com.wire.kalium.network.api.authenticated.notification.conversation.MessageEventData
+import io.ktor.util.encodeBase64
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.datetime.Clock
+
+class EventGenerator(private val selfClient: QualifiedClientID, targetClient: QualifiedClientID, val proteusClient: ProteusClient) {
+
+ private val protoContentMapper: ProtoContentMapper = MapperProvider.protoContentMapper(selfClient.userId)
+ private val targetSessionId = CryptoSessionId(
+ targetClient.userId.toCrypto(),
+ CryptoClientId(targetClient.clientId.value)
+ )
+ private val selfSessionId = CryptoSessionId(
+ selfClient.userId.toCrypto(),
+ CryptoClientId(selfClient.clientId.value)
+ )
+
+ fun generateEvents(
+ limit: Int,
+ conversationId: ConversationId,
+ ): Flow {
+ return flow {
+ repeat(limit) { count ->
+ val protobuf = generateProtoContent(generateTextContent(count))
+ val message = encryptMessage(protobuf, proteusClient, selfSessionId, targetSessionId)
+ val event = generateNewMessageDTO(selfClient.userId, conversationId, message)
+ emit(generateEventResponse(event))
+ }
+ }
+ }
+
+ private fun generateTextContent(
+ index: Int
+ ): MessageContent.Text {
+ return MessageContent.Text("Message $index")
+ }
+
+ private fun generateProtoContent(
+ messageContent: MessageContent.FromProto
+ ): PlainMessageBlob {
+ return protoContentMapper.encodeToProtobuf(
+ ProtoContent.Readable(
+ messageUid = uuid4().toString(),
+ messageContent = messageContent,
+ expectsReadConfirmation = true,
+ legalHoldStatus = Conversation.LegalHoldStatus.DISABLED,
+ expiresAfterMillis = null
+ )
+ )
+ }
+
+ private suspend fun encryptMessage(
+ message: PlainMessageBlob,
+ proteusClient: ProteusClient,
+ sender: CryptoSessionId,
+ recipient: CryptoSessionId
+ ): MessageEventData {
+ return MessageEventData(
+ text = proteusClient.encrypt(message.data, recipient).encodeBase64(),
+ sender = sender.cryptoClientId.value,
+ recipient = recipient.cryptoClientId.value,
+ encryptedExternalData = null
+ )
+ }
+
+ private fun generateNewMessageDTO(
+ from: UserId,
+ conversationId: ConversationId,
+ data: MessageEventData
+ ): EventContentDTO.Conversation.NewMessageDTO {
+ return EventContentDTO.Conversation.NewMessageDTO(
+ qualifiedConversation = conversationId.toApi(),
+ qualifiedFrom = from.toApi(),
+ conversation = conversationId.toPlainID().value,
+ from = from.toPlainID().value,
+ time = Clock.System.now(),
+ data = data
+ )
+
+ }
+
+ private fun generateEventResponse(event: EventContentDTO): EventResponse {
+ return EventResponse(
+ id = uuid4().toString(), // TODO jacob (should actually be UUIDv1)
+ payload = listOf(event),
+ transient = true // All events are transient to avoid persisting an incorrect last event id
+ )
+ }
+
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt
index d907751c978..9bbffa82e2c 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt
@@ -23,10 +23,12 @@ import com.wire.kalium.logic.data.client.ClientMapper
import com.wire.kalium.logic.data.connection.ConnectionMapper
import com.wire.kalium.logic.data.conversation.ClientId
import com.wire.kalium.logic.data.conversation.Conversation
+import com.wire.kalium.logic.data.conversation.ConversationMapper
import com.wire.kalium.logic.data.conversation.ConversationRoleMapper
import com.wire.kalium.logic.data.conversation.MemberMapper
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.conversation.ReceiptModeMapper
+import com.wire.kalium.logic.data.conversation.folders.toFolder
import com.wire.kalium.logic.data.conversation.toModel
import com.wire.kalium.logic.data.event.Event.UserProperty.ReadReceiptModeSet
import com.wire.kalium.logic.data.event.Event.UserProperty.TypingIndicatorModeSet
@@ -64,7 +66,8 @@ class EventMapper(
private val selfUserId: UserId,
private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper(),
private val clientMapper: ClientMapper = MapperProvider.clientMapper(),
- private val qualifiedIdMapper: QualifiedIdMapper = MapperProvider.qualifiedIdMapper(selfUserId)
+ private val qualifiedIdMapper: QualifiedIdMapper = MapperProvider.qualifiedIdMapper(selfUserId),
+ private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId)
) {
fun fromDTO(eventResponse: EventResponse, isLive: Boolean): List {
// TODO(edge-case): Multiple payloads in the same event have the same ID, is this an issue when marking lastProcessedEventId?
@@ -94,7 +97,7 @@ class EventMapper(
is EventContentDTO.User.LegalHoldDisabledDTO -> legalHoldDisabled(id, eventContentDTO)
is EventContentDTO.FeatureConfig.FeatureConfigUpdatedDTO -> featureConfig(id, eventContentDTO)
is EventContentDTO.Unknown -> unknown(id, eventContentDTO)
- is EventContentDTO.Conversation.AccessUpdate -> unknown(id, eventContentDTO)
+ is EventContentDTO.Conversation.AccessUpdate -> conversationAccessUpdate(id, eventContentDTO)
is EventContentDTO.Conversation.DeletedConversationDTO -> conversationDeleted(id, eventContentDTO)
is EventContentDTO.Conversation.ConversationRenameDTO -> conversationRenamed(id, eventContentDTO)
is EventContentDTO.Team.MemberLeave -> teamMemberLeft(id, eventContentDTO)
@@ -194,6 +197,17 @@ class EventMapper(
dateTime = eventContentDTO.time
)
+ private fun conversationAccessUpdate(
+ id: String,
+ eventContentDTO: EventContentDTO.Conversation.AccessUpdate
+ ): Event = Event.Conversation.AccessUpdate(
+ id = id,
+ conversationId = eventContentDTO.qualifiedConversation.toModel(),
+ access = conversationMapper.fromApiModelToAccessModel(eventContentDTO.data.access),
+ accessRole = conversationMapper.fromApiModelToAccessRoleModel(eventContentDTO.data.accessRole),
+ qualifiedFrom = eventContentDTO.qualifiedFrom.toModel()
+ )
+
private fun conversationReceiptModeUpdate(
id: String,
eventContentDTO: EventContentDTO.Conversation.ReceiptModeUpdate,
@@ -210,8 +224,8 @@ class EventMapper(
): Event {
val fieldKeyValue = eventContentDTO.value
val key = eventContentDTO.key
- return when {
- fieldKeyValue is EventContentDTO.FieldKeyNumberValue -> {
+ return when (fieldKeyValue) {
+ is EventContentDTO.FieldKeyNumberValue -> {
when (key) {
WIRE_RECEIPT_MODE.key -> ReadReceiptModeSet(
id,
@@ -231,7 +245,12 @@ class EventMapper(
}
}
- else -> unknown(
+ is EventContentDTO.FieldLabelListValue -> Event.UserProperty.FoldersUpdate(
+ id = id,
+ folders = fieldKeyValue.value.labels.map { it.toFolder(selfUserId.domain) }
+ )
+
+ is EventContentDTO.FieldUnknownValue -> unknown(
id = id,
eventContentDTO = eventContentDTO,
cause = "Unknown value type for key: ${eventContentDTO.key} "
@@ -437,7 +456,7 @@ class EventMapper(
}
@Suppress("MagicNumber")
- private fun mapConversationMutedStatus(status: Int?) = when (status) {
+ private fun mapConversationMutedStatus(status: Int?): MutedConversationStatus = when (status) {
0 -> MutedConversationStatus.AllAllowed
1 -> MutedConversationStatus.OnlyMentionsAndRepliesAllowed
3 -> MutedConversationStatus.AllMuted
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt
index 8f4fca7ce28..a91b00d2225 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt
@@ -22,35 +22,41 @@ import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.conversation.ClientId
+import com.wire.kalium.logic.data.id.CurrentClientIdProvider
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.di.MapperProvider
-import com.wire.kalium.logic.data.id.CurrentClientIdProvider
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.fold
import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.logic.wrapStorageRequest
-import com.wire.kalium.network.api.base.authenticated.notification.NotificationApi
import com.wire.kalium.network.api.authenticated.notification.NotificationResponse
+import com.wire.kalium.network.api.base.authenticated.notification.NotificationApi
import com.wire.kalium.network.api.base.authenticated.notification.WebSocketEvent
import com.wire.kalium.network.utils.NetworkResponse
import com.wire.kalium.network.utils.isSuccessful
import com.wire.kalium.persistence.dao.MetadataDAO
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.asFlow
-import kotlinx.coroutines.flow.flattenConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
+import kotlinx.serialization.json.Json
import kotlin.coroutines.coroutineContext
interface EventRepository {
+
suspend fun pendingEvents(): Flow>
suspend fun liveEvents(): Either>>
suspend fun updateLastProcessedEventId(eventId: String): Either
+ /**
+ * Parse events from an external JSON payload
+ *
+ * @return List of [EventEnvelope]
+ */
+ fun parseExternalEvents(data: String): List
+
/**
* Retrieves the last processed event ID from the storage.
*
@@ -76,6 +82,7 @@ interface EventRepository {
* @return Either containing a [CoreFailure] or the oldest available event ID as a String.
*/
suspend fun fetchOldestAvailableEventId(): Either
+ suspend fun fetchServerTime(): String?
}
class EventDataSource(
@@ -94,26 +101,31 @@ class EventDataSource(
currentClientId().flatMap { clientId -> liveEventsFlow(clientId) }
private suspend fun liveEventsFlow(clientId: ClientId): Either>> =
- wrapApiRequest { notificationApi.listenToLiveEvents(clientId.value) }.map {
- it.map { webSocketEvent ->
- when (webSocketEvent) {
- is WebSocketEvent.Open -> {
- flowOf(WebSocketEvent.Open())
- }
-
- is WebSocketEvent.NonBinaryPayloadReceived -> {
- flowOf(WebSocketEvent.NonBinaryPayloadReceived(webSocketEvent.payload))
- }
-
- is WebSocketEvent.Close -> {
- flowOf(WebSocketEvent.Close(webSocketEvent.cause))
- }
-
- is WebSocketEvent.BinaryPayloadReceived -> {
- eventMapper.fromDTO(webSocketEvent.payload, true).asFlow().map { WebSocketEvent.BinaryPayloadReceived(it) }
+ wrapApiRequest { notificationApi.listenToLiveEvents(clientId.value) }.map { webSocketEventFlow ->
+ flow {
+ webSocketEventFlow.collect { webSocketEvent ->
+ when (webSocketEvent) {
+ is WebSocketEvent.Open -> {
+ emit(WebSocketEvent.Open())
+ }
+
+ is WebSocketEvent.NonBinaryPayloadReceived -> {
+ emit(WebSocketEvent.NonBinaryPayloadReceived(webSocketEvent.payload))
+ }
+
+ is WebSocketEvent.Close -> {
+ emit(WebSocketEvent.Close(webSocketEvent.cause))
+ }
+
+ is WebSocketEvent.BinaryPayloadReceived -> {
+ val events = eventMapper.fromDTO(webSocketEvent.payload, true)
+ events.forEach { eventEnvelope ->
+ emit(WebSocketEvent.BinaryPayloadReceived(eventEnvelope))
+ }
+ }
}
}
- }.flattenConcat()
+ }
}
private suspend fun pendingEventsFlow(
@@ -145,6 +157,13 @@ class EventDataSource(
}
}
+ override fun parseExternalEvents(data: String): List {
+ val notificationResponse = Json.decodeFromString(data)
+ return notificationResponse.notifications.flatMap {
+ eventMapper.fromDTO(it, isLive = false)
+ }
+ }
+
override suspend fun lastProcessedEventId(): Either = wrapStorageRequest {
metadataDAO.valueByKey(LAST_PROCESSED_EVENT_ID_KEY)
}
@@ -177,6 +196,15 @@ class EventDataSource(
}
}.map { it.id }
+ override suspend fun fetchServerTime(): String? {
+ val result = notificationApi.getServerTime(NOTIFICATIONS_QUERY_SIZE)
+ return if (result.isSuccessful()) {
+ result.value
+ } else {
+ null
+ }
+ }
+
private companion object {
const val NOTIFICATIONS_QUERY_SIZE = 100
const val LAST_PROCESSED_EVENT_ID_KEY = "last_processed_event_id"
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt
index 32044174038..c28f15eea3a 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt
@@ -375,6 +375,7 @@ class MessageMapperImpl(
// We store the encoded data in case we decide to try to decrypt them again in the future
is MessageContent.FailedDecryption -> MessageEntityContent.FailedDecryption(
regularMessage.encodedData,
+ regularMessage.errorCode,
regularMessage.isDecryptionResolved,
regularMessage.senderUserId.toDao(),
regularMessage.clientId?.value
@@ -611,6 +612,7 @@ fun MessageEntityContent.Regular.toMessageContent(hidden: Boolean, selfUserId: U
is MessageEntityContent.Unknown -> MessageContent.Unknown(this.typeName, this.encodedData, hidden)
is MessageEntityContent.FailedDecryption -> MessageContent.FailedDecryption(
this.encodedData,
+ this.code,
this.isDecryptionResolved,
this.senderUserId.toModel(),
ClientId(this.senderClientId.orEmpty())
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt
index 66ce73703f3..dd0f47b5996 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt
@@ -80,7 +80,6 @@ internal interface MessageRepository {
)
suspend fun persistMessage(
message: Message.Standalone,
- updateConversationReadDate: Boolean = false,
updateConversationModifiedDate: Boolean = false,
): Either
@@ -321,12 +320,10 @@ internal class MessageDataSource internal constructor(
)
override suspend fun persistMessage(
message: Message.Standalone,
- updateConversationReadDate: Boolean,
updateConversationModifiedDate: Boolean,
): Either = wrapStorageRequest {
messageDAO.insertOrIgnoreMessage(
messageMapper.fromMessageToEntity(message),
- updateConversationReadDate,
updateConversationModifiedDate
)
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt
index eebc9246e5d..0d0288de34f 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt
@@ -43,11 +43,10 @@ internal class PersistMessageUseCaseImpl(
) : PersistMessageUseCase {
override suspend operator fun invoke(message: Message.Standalone): Either {
val modifiedMessage = getExpectsReadConfirmationFromMessage(message)
-
val isSelfSender = message.isSelfTheSender(selfUserId)
+
return messageRepository.persistMessage(
message = modifiedMessage,
- updateConversationReadDate = isSelfSender,
updateConversationModifiedDate = message.content.shouldUpdateConversationOrder()
).onSuccess {
val isConversationMuted = it == InsertMessageResult.INSERTED_INTO_MUTED_CONVERSATION
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt
index 6de692f0b05..036fa81bf0f 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt
@@ -57,6 +57,7 @@ import com.wire.kalium.protobuf.messages.Quote
import com.wire.kalium.protobuf.messages.Reaction
import com.wire.kalium.protobuf.messages.Text
import com.wire.kalium.protobuf.messages.TrackingIdentifier
+import com.wire.kalium.protobuf.messages.UnknownStrategy
import kotlinx.datetime.Instant
import pbandk.ByteArr
@@ -82,7 +83,10 @@ class ProtoContentMapperImpl(
is ProtoContent.Readable -> mapReadableContentToProtobuf(protoContent)
}
- val message = GenericMessage(protoContent.messageUid, messageContent)
+ val message = GenericMessage(
+ messageId = protoContent.messageUid,
+ content = messageContent
+ )
return PlainMessageBlob(message.encodeToByteArray())
}
@@ -374,8 +378,17 @@ class ProtoContentMapperImpl(
}
null -> {
- kaliumLogger.w("Null content when parsing protobuf. Message UUID = ${genericMessage.messageId.obfuscateId()}")
- MessageContent.Ignored
+ kaliumLogger.w(
+ "Null content when parsing protobuf. Message UUID = ${genericMessage.messageId.obfuscateId()}" +
+ " Message Unknown Strategy = ${genericMessage.unknownStrategy}"
+ )
+ when (genericMessage.unknownStrategy) {
+ UnknownStrategy.DISCARD_AND_WARN -> MessageContent.Unknown()
+ UnknownStrategy.WARN_USER_ALLOW_RETRY -> MessageContent.Unknown(encodedData = encodedContent.data)
+ UnknownStrategy.IGNORE,
+ is UnknownStrategy.UNRECOGNIZED,
+ null -> MessageContent.Ignored
+ }
}
}
return readableContent
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt
index abc2ede9218..892b4c26511 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt
@@ -53,7 +53,8 @@ internal class ReactionsMapperImpl(
userTypeEntity = userType,
deleted = deleted,
connectionStatus = connectionStatus,
- availabilityStatus = userAvailabilityStatus
+ availabilityStatus = userAvailabilityStatus,
+ accentId = accentId
)
}
@@ -71,7 +72,8 @@ internal class ReactionsMapperImpl(
userType = domainUserTypeMapper.fromUserTypeEntity(userTypeEntity),
isUserDeleted = deleted,
connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus),
- availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus)
+ availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus),
+ accentId = accentId
)
)
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt
index be3fd5a56ef..6aacce40d91 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt
@@ -65,7 +65,8 @@ internal class ReceiptsMapperImpl(
userType = domainUserTypeMapper.fromUserTypeEntity(userType),
isUserDeleted = isUserDeleted,
connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus),
- availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus)
+ availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus),
+ accentId = accentId
)
)
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt
index 48709255f9b..a54389074d1 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt
@@ -21,6 +21,7 @@ package com.wire.kalium.logic.data.mlspublickeys
import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.MLSFailure
import com.wire.kalium.logic.data.mls.CipherSuite
+import com.wire.kalium.logic.data.mls.MLSPublicKeys
import com.wire.kalium.logic.di.MapperProvider
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMap
@@ -30,9 +31,14 @@ import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.network.api.base.authenticated.serverpublickey.MLSPublicKeyApi
import io.ktor.util.decodeBase64Bytes
-data class MLSPublicKeys(
- val removal: Map?
-)
+fun MLSPublicKeys.getRemovalKey(cipherSuite: CipherSuite): Either {
+ val mlsPublicKeysMapper: MLSPublicKeysMapper = MapperProvider.mlsPublicKeyMapper()
+ val keySignature = mlsPublicKeysMapper.fromCipherSuite(cipherSuite)
+ val key = this.removal?.let { removalKeys ->
+ removalKeys[keySignature.value]
+ } ?: return Either.Left(MLSFailure.Generic(IllegalStateException("No key found for cipher suite $cipherSuite")))
+ return key.decodeBase64Bytes().right()
+}
interface MLSPublicKeysRepository {
suspend fun fetchKeys(): Either
@@ -42,7 +48,6 @@ interface MLSPublicKeysRepository {
class MLSPublicKeysRepositoryImpl(
private val mlsPublicKeyApi: MLSPublicKeyApi,
- private val mlsPublicKeysMapper: MLSPublicKeysMapper = MapperProvider.mlsPublicKeyMapper()
) : MLSPublicKeysRepository {
// TODO: make it thread safe
@@ -60,14 +65,8 @@ class MLSPublicKeysRepositoryImpl(
}
override suspend fun getKeyForCipherSuite(cipherSuite: CipherSuite): Either {
-
return getKeys().flatMap { serverPublicKeys ->
- val keySignature = mlsPublicKeysMapper.fromCipherSuite(cipherSuite)
- val key = serverPublicKeys.removal?.let { removalKeys ->
- removalKeys[keySignature.value]
- } ?: return Either.Left(MLSFailure.Generic(IllegalStateException("No key found for cipher suite $cipherSuite")))
- key.decodeBase64Bytes().right()
+ serverPublicKeys.getRemovalKey(cipherSuite)
}
}
-
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt
index 0f7700af816..ce30758bd3c 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt
@@ -20,9 +20,13 @@ package com.wire.kalium.logic.data.properties
import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.configuration.UserConfigRepository
+import com.wire.kalium.logic.data.conversation.FolderWithConversations
+import com.wire.kalium.logic.data.conversation.folders.toFolder
+import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.flatMap
import com.wire.kalium.logic.functional.fold
+import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi
import com.wire.kalium.network.api.authenticated.properties.PropertyKey
@@ -38,11 +42,13 @@ interface UserPropertyRepository {
suspend fun observeTypingIndicatorStatus(): Flow>
suspend fun setTypingIndicatorEnabled(): Either
suspend fun removeTypingIndicatorProperty(): Either
+ suspend fun getConversationFolders(): Either>
}
internal class UserPropertyDataSource(
private val propertiesApi: PropertiesApi,
private val userConfigRepository: UserConfigRepository,
+ private val selfUserId: UserId
) : UserPropertyRepository {
override suspend fun getReadReceiptsStatus(): Boolean =
userConfigRepository.isReadReceiptsEnabled()
@@ -82,4 +88,9 @@ internal class UserPropertyDataSource(
}.flatMap {
userConfigRepository.setTypingIndicatorStatus(false)
}
+
+ override suspend fun getConversationFolders(): Either> = wrapApiRequest {
+ propertiesApi.getLabels()
+ }
+ .map { it.labels.map { label -> label.toFolder(selfUserId.domain) } }
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt
index 8092e6f3e4b..ef9c8eee0de 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt
@@ -107,6 +107,7 @@ internal class SessionMapperImpl : SessionMapper {
LogoutReason.REMOVED_CLIENT -> LogoutReasonEntity.REMOVED_CLIENT
LogoutReason.DELETED_ACCOUNT -> LogoutReasonEntity.DELETED_ACCOUNT
LogoutReason.SESSION_EXPIRED -> LogoutReasonEntity.SESSION_EXPIRED
+ LogoutReason.MIGRATION_TO_CC_FAILED -> LogoutReasonEntity.MIGRATION_TO_CC_FAILED
}
override fun toSsoIdEntity(ssoId: SsoId?): SsoIdEntity? =
@@ -140,6 +141,7 @@ internal class SessionMapperImpl : SessionMapper {
LogoutReasonEntity.REMOVED_CLIENT -> LogoutReason.REMOVED_CLIENT
LogoutReasonEntity.DELETED_ACCOUNT -> LogoutReason.DELETED_ACCOUNT
LogoutReasonEntity.SESSION_EXPIRED -> LogoutReason.SESSION_EXPIRED
+ LogoutReasonEntity.MIGRATION_TO_CC_FAILED -> LogoutReason.MIGRATION_TO_CC_FAILED
}
override fun fromEntityToProxyCredentialsDTO(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentialsDTO =
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt
index 2820a318f7b..a095eb3b5d3 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt
@@ -19,6 +19,7 @@
package com.wire.kalium.logic.data.sync
import com.wire.kalium.logic.CoreFailure
+import kotlin.time.Duration
sealed interface IncrementalSyncStatus {
@@ -34,8 +35,8 @@ sealed interface IncrementalSyncStatus {
override fun toString() = "LIVE"
}
- data class Failed(val failure: CoreFailure) : IncrementalSyncStatus {
- override fun toString() = "FAILED, cause: '$failure'"
+ data class Failed(val failure: CoreFailure, val retryDelay: Duration) : IncrementalSyncStatus {
+ override fun toString() = "FAILED, cause: '$failure' retry in: $retryDelay"
}
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt
index 77affb1fbc8..5bb50ed3cf4 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt
@@ -19,6 +19,7 @@
package com.wire.kalium.logic.data.sync
import com.wire.kalium.logic.CoreFailure
+import kotlin.time.Duration
sealed interface SlowSyncStatus {
@@ -28,7 +29,7 @@ sealed interface SlowSyncStatus {
data class Ongoing(val currentStep: SlowSyncStep) : SlowSyncStatus
- data class Failed(val failure: CoreFailure) : SlowSyncStatus
+ data class Failed(val failure: CoreFailure, val retryDelay: Duration) : SlowSyncStatus
}
enum class SlowSyncStep {
@@ -43,4 +44,5 @@ enum class SlowSyncStep {
JOINING_MLS_CONVERSATIONS,
RESOLVE_ONE_ON_ONE_PROTOCOLS,
LEGAL_HOLD,
+ CONVERSATION_FOLDERS,
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt
index 925abda6a5a..a3a5fd0d67e 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt
@@ -19,6 +19,7 @@
package com.wire.kalium.logic.data.sync
import com.wire.kalium.logic.CoreFailure
+import kotlin.time.Duration
sealed class SyncState {
@@ -53,6 +54,7 @@ sealed class SyncState {
/**
* Sync was not completed due to a failure.
+ * [retryDelay] specifies the duration in which next try will happen
*/
- data class Failed(val cause: CoreFailure) : SyncState()
+ data class Failed(val cause: CoreFailure, val retryDelay: Duration) : SyncState()
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt
index 69a8fad273d..cd8398f0f5c 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt
@@ -198,6 +198,7 @@ internal class UserMapperImpl(
name = userEntity.name,
completePicture = userEntity.completeAssetId?.toModel(),
userType = domainUserTypeMapper.fromUserTypeEntity(userEntity.userType),
+ accentId = userEntity.accentId
)
override fun fromEntityToUserSummary(userEntity: UserEntity) = with(userEntity) {
@@ -209,7 +210,8 @@ internal class UserMapperImpl(
userType = domainUserTypeMapper.fromUserTypeEntity(userType),
isUserDeleted = deleted,
availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus),
- connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus)
+ connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus),
+ accentId = accentId
)
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt
index 03944f0a9b9..e8760f83b79 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt
@@ -25,6 +25,7 @@ import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.conversation.MemberMapper
import com.wire.kalium.logic.data.conversation.Recipient
+import com.wire.kalium.logic.data.conversation.mls.NameAndHandle
import com.wire.kalium.logic.data.event.Event
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.IdMapper
@@ -63,6 +64,7 @@ import com.wire.kalium.network.api.authenticated.userDetails.ListUserRequest
import com.wire.kalium.network.api.authenticated.userDetails.ListUsersDTO
import com.wire.kalium.network.api.authenticated.userDetails.qualifiedIds
import com.wire.kalium.network.api.base.authenticated.TeamsApi
+import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi
import com.wire.kalium.network.api.base.authenticated.self.SelfApi
import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi
import com.wire.kalium.network.api.model.LegalHoldStatusDTO
@@ -162,6 +164,9 @@ interface UserRepository {
suspend fun getOneOnOnConversationId(userId: QualifiedID): Either
suspend fun getUsersMinimizedByQualifiedIDs(userIds: List): Either>
+ suspend fun getNameAndHandle(userId: UserId): Either
+ suspend fun migrateUserToTeam(teamName: String): Either
+ suspend fun updateTeamId(userId: UserId, teamId: TeamId): Either
}
@Suppress("LongParameterList", "TooManyFunctions")
@@ -171,6 +176,7 @@ internal class UserDataSource internal constructor(
private val clientDAO: ClientDAO,
private val selfApi: SelfApi,
private val userDetailsApi: UserDetailsApi,
+ private val upgradePersonalToTeamApi: UpgradePersonalToTeamApi,
private val teamsApi: TeamsApi,
private val sessionRepository: SessionRepository,
private val selfUserId: UserId,
@@ -641,6 +647,27 @@ internal class UserDataSource internal constructor(
userDAO.getOneOnOnConversationId(userId.toDao())?.toModel()
}
+ override suspend fun getNameAndHandle(userId: UserId): Either = wrapStorageRequest {
+ userDAO.getNameAndHandle(userId.toDao())
+ }.map { NameAndHandle.fromEntity(it) }
+
+ override suspend fun migrateUserToTeam(teamName: String): Either {
+ return wrapApiRequest { upgradePersonalToTeamApi.migrateToTeam(teamName) }.map { dto ->
+ CreateUserTeam(dto.teamId, dto.teamName)
+ }
+ .onSuccess {
+ kaliumLogger.d("Migrated user to team")
+ fetchSelfUser()
+ }
+ .onFailure { failure ->
+ kaliumLogger.e("Failed to migrate user to team: $failure")
+ }
+ }
+
+ override suspend fun updateTeamId(userId: UserId, teamId: TeamId): Either = wrapStorageRequest {
+ userDAO.updateTeamId(userId.toDao(), teamId.value)
+ }
+
companion object {
internal const val SELF_USER_ID_KEY = "selfUserID"
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt
index bd1b7939132..52e90b92103 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt
@@ -128,7 +128,8 @@ internal object MapperProvider {
AvailabilityStatusMapperImpl(),
DomainUserTypeMapperImpl(),
ConnectionStatusMapperImpl(),
- ConversationRoleMapperImpl()
+ ConversationRoleMapperImpl(),
+ MessageMapperImpl(selfUserId),
)
fun conversationRoleMapper(): ConversationRoleMapper = ConversationRoleMapperImpl()
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt
index 68a353dc754..e7e1a1637a8 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt
@@ -51,6 +51,7 @@ import com.wire.kalium.logic.data.client.MLSClientProvider
import com.wire.kalium.logic.data.client.MLSClientProviderImpl
import com.wire.kalium.logic.data.client.ProteusClientProvider
import com.wire.kalium.logic.data.client.ProteusClientProviderImpl
+import com.wire.kalium.logic.data.client.ProteusMigrationRecoveryHandler
import com.wire.kalium.logic.data.client.remote.ClientRemoteDataSource
import com.wire.kalium.logic.data.client.remote.ClientRemoteRepository
import com.wire.kalium.logic.data.connection.ConnectionDataSource
@@ -78,6 +79,8 @@ import com.wire.kalium.logic.data.conversation.ProposalTimer
import com.wire.kalium.logic.data.conversation.SubconversationRepositoryImpl
import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProvider
import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProviderImpl
+import com.wire.kalium.logic.data.conversation.folders.ConversationFolderDataSource
+import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository
import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository
import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepositoryDataSource
import com.wire.kalium.logic.data.e2ei.E2EIRepository
@@ -153,13 +156,14 @@ import com.wire.kalium.logic.di.PlatformUserStorageProperties
import com.wire.kalium.logic.di.RootPathsProvider
import com.wire.kalium.logic.di.UserStorageProvider
import com.wire.kalium.logic.feature.analytics.AnalyticsIdentifierManager
+import com.wire.kalium.logic.feature.analytics.GetCurrentAnalyticsTrackingIdentifierUseCase
import com.wire.kalium.logic.feature.analytics.ObserveAnalyticsTrackingIdentifierStatusUseCase
import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserver
import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl
import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase
import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCaseImpl
-import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCase
-import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCaseImpl
+import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCase
+import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCaseImpl
import com.wire.kalium.logic.feature.auth.AuthenticationScope
import com.wire.kalium.logic.feature.auth.AuthenticationScopeProvider
import com.wire.kalium.logic.feature.auth.ClearUserDataUseCase
@@ -186,6 +190,7 @@ import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCase
import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCaseImpl
import com.wire.kalium.logic.feature.client.MLSClientManager
import com.wire.kalium.logic.feature.client.MLSClientManagerImpl
+import com.wire.kalium.logic.feature.client.ProteusMigrationRecoveryHandlerImpl
import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCase
import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCaseImpl
import com.wire.kalium.logic.feature.connection.ConnectionScope
@@ -205,6 +210,8 @@ import com.wire.kalium.logic.feature.conversation.RecoverMLSConversationsUseCase
import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCase
import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCaseImpl
import com.wire.kalium.logic.feature.conversation.TypingIndicatorSyncManager
+import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCase
+import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCaseImpl
import com.wire.kalium.logic.feature.conversation.keyingmaterials.KeyingMaterialsManager
import com.wire.kalium.logic.feature.conversation.keyingmaterials.KeyingMaterialsManagerImpl
import com.wire.kalium.logic.feature.conversation.mls.MLSOneOnOneConversationResolver
@@ -331,6 +338,8 @@ import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlag
import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCaseImpl
import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase
import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCaseImpl
+import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCase
+import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCaseImpl
import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase
import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCaseImpl
import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase
@@ -380,6 +389,7 @@ import com.wire.kalium.logic.sync.receiver.UserPropertiesEventReceiver
import com.wire.kalium.logic.sync.receiver.UserPropertiesEventReceiverImpl
import com.wire.kalium.logic.sync.receiver.asset.AssetMessageHandler
import com.wire.kalium.logic.sync.receiver.asset.AssetMessageHandlerImpl
+import com.wire.kalium.logic.sync.receiver.conversation.AccessUpdateEventHandler
import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandler
import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandlerImpl
import com.wire.kalium.logic.sync.receiver.conversation.DeletedConversationEventHandler
@@ -444,6 +454,7 @@ import com.wire.kalium.network.NetworkStateObserver
import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer
import com.wire.kalium.network.session.SessionManager
import com.wire.kalium.network.utils.MockUnboundNetworkClient
+import com.wire.kalium.network.utils.MockWebSocketSession
import com.wire.kalium.persistence.client.ClientRegistrationStorage
import com.wire.kalium.persistence.client.ClientRegistrationStorageImpl
import com.wire.kalium.persistence.db.GlobalDatabaseBuilder
@@ -551,6 +562,10 @@ class UserSessionScope internal constructor(
}
}
+ private val invalidateTeamId = {
+ _teamId = Either.Left(CoreFailure.Unknown(Throwable("NotInitialized")))
+ }
+
private val selfTeamId = SelfTeamIdProvider { teamId() }
private val accessTokenRepository: AccessTokenRepository
@@ -584,10 +599,10 @@ class UserSessionScope internal constructor(
userAgent = userAgent,
certificatePinning = kaliumConfigs.certPinningConfig,
mockEngine = kaliumConfigs.mockedRequests?.let { MockUnboundNetworkClient.createMockEngine(it) },
+ mockWebSocketSession = if (kaliumConfigs.mockedWebSocket) MockWebSocketSession() else null,
kaliumLogger = userScopedLogger
)
private val featureSupport: FeatureSupport = FeatureSupportImpl(
- kaliumConfigs,
sessionManager.serverConfig().metaData.commonApiVersion.version
)
@@ -610,7 +625,8 @@ class UserSessionScope internal constructor(
private val userPropertyRepository: UserPropertyRepository
get() = UserPropertyDataSource(
authenticatedNetworkContainer.propertiesApi,
- userConfigRepository
+ userConfigRepository,
+ userId
)
private val keyPackageLimitsProvider: KeyPackageLimitsProvider
@@ -619,12 +635,17 @@ class UserSessionScope internal constructor(
private val updateKeyingMaterialThresholdProvider: UpdateKeyingMaterialThresholdProvider
get() = UpdateKeyingMaterialThresholdProviderImpl(kaliumConfigs)
+ private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler by lazy {
+ ProteusMigrationRecoveryHandlerImpl(lazy { logout })
+ }
+
val proteusClientProvider: ProteusClientProvider by lazy {
ProteusClientProviderImpl(
rootProteusPath = rootPathsProvider.rootProteusPath(userId),
userId = userId,
passphraseStorage = globalPreferences.passphraseStorage,
- kaliumConfigs = kaliumConfigs
+ kaliumConfigs = kaliumConfigs,
+ proteusMigrationRecoveryHandler = proteusMigrationRecoveryHandler
)
}
@@ -692,7 +713,8 @@ class UserSessionScope internal constructor(
private val notificationTokenRepository get() = NotificationTokenDataSource(globalPreferences.tokenStorage)
- private val subconversationRepository = SubconversationRepositoryImpl()
+ private val subconversationRepository =
+ SubconversationRepositoryImpl(conversationApi = authenticatedNetworkContainer.conversationApi)
private val conversationRepository: ConversationRepository
get() = ConversationDataSource(
@@ -703,10 +725,17 @@ class UserSessionScope internal constructor(
userStorage.database.memberDAO,
authenticatedNetworkContainer.conversationApi,
userStorage.database.messageDAO,
+ userStorage.database.messageDraftDAO,
userStorage.database.clientDAO,
authenticatedNetworkContainer.clientApi,
userStorage.database.conversationMetaDataDAO,
- userStorage.database.messageDraftDAO
+ )
+
+ private val conversationFolderRepository: ConversationFolderRepository
+ get() = ConversationFolderDataSource(
+ userStorage.database.conversationFolderDAO,
+ authenticatedNetworkContainer.propertiesApi,
+ userId
)
private val conversationGroupRepository: ConversationGroupRepository
@@ -778,16 +807,17 @@ class UserSessionScope internal constructor(
)
private val userRepository: UserRepository = UserDataSource(
- userStorage.database.userDAO,
- userStorage.database.metadataDAO,
- userStorage.database.clientDAO,
- authenticatedNetworkContainer.selfApi,
- authenticatedNetworkContainer.userDetailsApi,
- authenticatedNetworkContainer.teamsApi,
- globalScope.sessionRepository,
- userId,
- selfTeamId,
- legalHoldHandler
+ userDAO = userStorage.database.userDAO,
+ metadataDAO = userStorage.database.metadataDAO,
+ clientDAO = userStorage.database.clientDAO,
+ selfApi = authenticatedNetworkContainer.selfApi,
+ userDetailsApi = authenticatedNetworkContainer.userDetailsApi,
+ upgradePersonalToTeamApi = authenticatedNetworkContainer.upgradePersonalToTeamApi,
+ teamsApi = authenticatedNetworkContainer.teamsApi,
+ sessionRepository = globalScope.sessionRepository,
+ selfUserId = userId,
+ selfTeamIdProvider = selfTeamId,
+ legalHoldHandler = legalHoldHandler,
)
private val accountRepository: AccountRepository
@@ -913,22 +943,24 @@ class UserSessionScope internal constructor(
kaliumFileSystem = kaliumFileSystem
)
- private val eventGatherer: EventGatherer get() = EventGathererImpl(
- eventRepository,
- incrementalSyncRepository,
- userScopedLogger,
- )
+ private val eventGatherer: EventGatherer
+ get() = EventGathererImpl(
+ eventRepository = eventRepository,
+ incrementalSyncRepository = incrementalSyncRepository,
+ logger = userScopedLogger
+ )
private val eventProcessor: EventProcessor by lazy {
EventProcessorImpl(
- eventRepository,
- conversationEventReceiver,
- userEventReceiver,
- teamEventReceiver,
- featureConfigEventReceiver,
- userPropertiesEventReceiver,
- federationEventReceiver,
- userScopedLogger,
+ eventRepository = eventRepository,
+ conversationEventReceiver = conversationEventReceiver,
+ userEventReceiver = userEventReceiver,
+ teamEventReceiver = teamEventReceiver,
+ featureConfigEventReceiver = featureConfigEventReceiver,
+ userPropertiesEventReceiver = userPropertiesEventReceiver,
+ federationEventReceiver = federationEventReceiver,
+ processingScope = this@UserSessionScope,
+ logger = userScopedLogger,
)
}
@@ -949,6 +981,9 @@ class UserSessionScope internal constructor(
systemMessageInserter
)
+ private val syncConversationFolders: SyncConversationFoldersUseCase
+ get() = SyncConversationFoldersUseCaseImpl(conversationFolderRepository)
+
private val syncConnections: SyncConnectionsUseCase
get() = SyncConnectionsUseCaseImpl(
connectionRepository = connectionRepository
@@ -1028,7 +1063,8 @@ class UserSessionScope internal constructor(
conversationGroupRepository,
conversationRepository,
messageRepository,
- userRepository
+ userRepository,
+ systemMessageInserter
)
private val oneOnOneResolver: OneOnOneResolver
get() = OneOnOneResolverImpl(
@@ -1066,7 +1102,8 @@ class UserSessionScope internal constructor(
syncContacts,
joinExistingMLSConversations,
fetchLegalHoldForSelfUserFromRemoteUseCase,
- oneOnOneResolver
+ oneOnOneResolver,
+ syncConversationFolders
)
}
@@ -1219,7 +1256,6 @@ class UserSessionScope internal constructor(
private val pendingProposalScheduler: PendingProposalScheduler =
PendingProposalSchedulerImpl(
- kaliumConfigs,
incrementalSyncRepository,
lazy { mlsConversationRepository },
lazy { subconversationRepository }
@@ -1232,6 +1268,7 @@ class UserSessionScope internal constructor(
userRepository = userRepository,
currentClientIdProvider = clientIdProvider,
conversationRepository = conversationRepository,
+ userConfigRepository = userConfigRepository,
selfConversationIdProvider = selfConversationIdProvider,
messageSender = messages.messageSender,
federatedIdMapper = federatedIdMapper,
@@ -1461,6 +1498,12 @@ class UserSessionScope internal constructor(
callRepository = callRepository
)
+ private val conversationAccessUpdateEventHandler: AccessUpdateEventHandler
+ get() = AccessUpdateEventHandler(
+ conversationDAO = userStorage.database.conversationDAO,
+ selfUserId = userId
+ )
+
private val conversationEventReceiver: ConversationEventReceiver by lazy {
ConversationEventReceiverImpl(
newMessageHandler,
@@ -1476,7 +1519,8 @@ class UserSessionScope internal constructor(
conversationCodeUpdateHandler,
conversationCodeDeletedHandler,
typingIndicatorHandler,
- protocolUpdateEventHandler
+ protocolUpdateEventHandler,
+ conversationAccessUpdateEventHandler
)
}
override val coroutineContext: CoroutineContext = SupervisorJob()
@@ -1492,6 +1536,9 @@ class UserSessionScope internal constructor(
val observeAnalyticsTrackingIdentifierStatus: ObserveAnalyticsTrackingIdentifierStatusUseCase
get() = ObserveAnalyticsTrackingIdentifierStatusUseCase(userConfigRepository, userScopedLogger)
+ val getCurrentAnalyticsTrackingIdentifier: GetCurrentAnalyticsTrackingIdentifierUseCase
+ get() = GetCurrentAnalyticsTrackingIdentifierUseCase(userConfigRepository)
+
val analyticsIdentifierManager: AnalyticsIdentifierManager
get() = AnalyticsIdentifierManager(
messages.messageSender,
@@ -1570,7 +1617,7 @@ class UserSessionScope internal constructor(
)
private val userPropertiesEventReceiver: UserPropertiesEventReceiver
- get() = UserPropertiesEventReceiverImpl(userConfigRepository)
+ get() = UserPropertiesEventReceiverImpl(userConfigRepository, conversationFolderRepository)
private val federationEventReceiver: FederationEventReceiver
get() = FederationEventReceiverImpl(
@@ -1738,6 +1785,7 @@ class UserSessionScope internal constructor(
conversationGroupRepository,
connectionRepository,
userRepository,
+ conversationFolderRepository,
syncManager,
mlsConversationRepository,
clientIdProvider,
@@ -1778,6 +1826,7 @@ class UserSessionScope internal constructor(
userRepository,
userId,
assetRepository,
+ eventRepository,
syncManager,
slowSyncRepository,
messageSendingScheduler,
@@ -1785,6 +1834,7 @@ class UserSessionScope internal constructor(
staleEpochVerifier,
eventProcessor,
legalHoldHandler,
+ notificationTokenRepository,
this,
userScopedLogger,
)
@@ -1842,6 +1892,7 @@ class UserSessionScope internal constructor(
clientIdProvider,
e2eiRepository,
mlsConversationRepository,
+ conversationRepository,
team.isSelfATeamMember,
updateSupportedProtocols,
clientRepository,
@@ -1850,6 +1901,8 @@ class UserSessionScope internal constructor(
isE2EIEnabled,
certificateRevocationListRepository,
incrementalSyncRepository,
+ sessionManager,
+ selfTeamId,
checkRevocationList,
syncFeatureConfigsUseCase,
userScopedLogger
@@ -1858,6 +1911,9 @@ class UserSessionScope internal constructor(
val search: SearchScope by lazy {
SearchScope(
+ mlsPublicKeysRepository = mlsPublicKeysRepository,
+ getDefaultProtocol = getDefaultProtocol,
+ getConversationProtocolInfo = conversations.getConversationProtocolInfo,
searchUserRepository = searchUserRepository,
selfUserId = userId,
sessionRepository = globalScope.sessionRepository,
@@ -1867,7 +1923,7 @@ class UserSessionScope internal constructor(
private val clearUserData: ClearUserDataUseCase get() = ClearUserDataUseCaseImpl(userStorage)
- private val validateAssetMimeType: ValidateAssetMimeTypeUseCase get() = ValidateAssetMimeTypeUseCaseImpl()
+ private val validateAssetMimeType: ValidateAssetFileTypeUseCase get() = ValidateAssetFileTypeUseCaseImpl()
val logout: LogoutUseCase
get() = LogoutUseCaseImpl(
@@ -1959,7 +2015,11 @@ class UserSessionScope internal constructor(
@OptIn(DelicateKaliumApi::class)
private val isAllowedToRegisterMLSClient: IsAllowedToRegisterMLSClientUseCase
- get() = IsAllowedToRegisterMLSClientUseCaseImpl(featureSupport, mlsPublicKeysRepository)
+ get() = IsAllowedToRegisterMLSClientUseCaseImpl(
+ featureSupport,
+ mlsPublicKeysRepository,
+ userConfigRepository
+ )
private val syncFeatureConfigsUseCase: SyncFeatureConfigsUseCase
get() = SyncFeatureConfigsUseCaseImpl(
@@ -1976,12 +2036,13 @@ class UserSessionScope internal constructor(
appLockConfigHandler
)
- val team: TeamScope get() = TeamScope(
- teamRepository = teamRepository,
- conversationRepository = conversationRepository,
- slowSyncRepository = slowSyncRepository,
- selfTeamIdProvider = selfTeamId
- )
+ val team: TeamScope
+ get() = TeamScope(
+ teamRepository = teamRepository,
+ conversationRepository = conversationRepository,
+ slowSyncRepository = slowSyncRepository,
+ selfTeamIdProvider = selfTeamId
+ )
val service: ServiceScope
get() = ServiceScope(
@@ -2050,6 +2111,9 @@ class UserSessionScope internal constructor(
userScopedLogger
)
+ val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase
+ get() = MigrateFromPersonalToTeamUseCaseImpl(userId, userRepository, invalidateTeamId)
+
internal val getProxyCredentials: GetProxyCredentialsUseCase
get() = GetProxyCredentialsUseCaseImpl(sessionManager)
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCase.kt
new file mode 100644
index 00000000000..03896e59a41
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCase.kt
@@ -0,0 +1,40 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.feature.analytics
+
+import com.wire.kalium.logic.configuration.UserConfigRepository
+
+/**
+ * Use case that returns the current analytics tracking identifier
+ */
+interface GetCurrentAnalyticsTrackingIdentifierUseCase {
+ /**
+ * Use case [GetCurrentAnalyticsTrackingIdentifierUseCase] operation
+ *
+ * @return a [String] containing the current tracking identifier
+ */
+ suspend operator fun invoke(): String?
+}
+
+@Suppress("FunctionNaming")
+internal fun GetCurrentAnalyticsTrackingIdentifierUseCase(
+ userConfigRepository: UserConfigRepository
+) = object : GetCurrentAnalyticsTrackingIdentifierUseCase {
+
+ override suspend fun invoke(): String? = userConfigRepository.getCurrentTrackingIdentifier()
+}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt
index c6b2a1ff0df..34655254acb 100644
--- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt
@@ -111,7 +111,7 @@ internal class ScheduleNewAssetMessageUseCaseImpl(
private val selfDeleteTimer: ObserveSelfDeletionTimerSettingsForConversationUseCase,
private val scope: CoroutineScope,
private val observeFileSharingStatus: ObserveFileSharingStatusUseCase,
- private val validateAssetMimeTypeUseCase: ValidateAssetMimeTypeUseCase,
+ private val validateAssetFileUseCase: ValidateAssetFileTypeUseCase,
private val dispatcher: KaliumDispatcher,
) : ScheduleNewAssetMessageUseCase {
@@ -133,7 +133,12 @@ internal class ScheduleNewAssetMessageUseCaseImpl(
FileSharingStatus.Value.Disabled -> return ScheduleNewAssetMessageResult.Failure.DisabledByTeam
FileSharingStatus.Value.EnabledAll -> { /* no-op*/ }
- is FileSharingStatus.Value.EnabledSome -> if (!validateAssetMimeTypeUseCase(assetMimeType, it.state.allowedType)) {
+ is FileSharingStatus.Value.EnabledSome -> if (!validateAssetFileUseCase(
+ fileName = assetName,
+ mimeType = assetMimeType,
+ allowedExtension = it.state.allowedType
+ )
+ ) {
kaliumLogger.e("The asset message trying to be processed has invalid content data")
return ScheduleNewAssetMessageResult.Failure.RestrictedFileType
}
diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCase.kt
new file mode 100644
index 00000000000..b4f887e1bb4
--- /dev/null
+++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCase.kt
@@ -0,0 +1,111 @@
+/*
+ * Wire
+ * Copyright (C) 2024 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.kalium.logic.feature.asset
+
+import com.wire.kalium.logic.kaliumLogger
+
+/**
+ * Returns true if the file extension is present in file name and is allowed and false otherwise.
+ * @param fileName the file name (with extension) to validate.
+ * @param allowedExtension the list of allowed extension.
+ */
+interface ValidateAssetFileTypeUseCase {
+ operator fun invoke(
+ fileName: String?,
+ mimeType: String,
+ allowedExtension: List
+ ): Boolean
+}
+
+internal class ValidateAssetFileTypeUseCaseImpl : ValidateAssetFileTypeUseCase {
+ override operator fun invoke(
+ fileName: String?,
+ mimeType: String,
+ allowedExtension: List