Merge tag 'v1.6.16' into sc

Change-Id: I690d21f0bac84dfed5d6f87e9c1aa30c78c8d346

Conflicts:
	dependencies_groups.gradle
	vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
This commit is contained in:
SpiritCroc 2024-05-30 20:16:19 +02:00
commit abdc558c07
66 changed files with 566 additions and 2853 deletions

View file

@ -58,27 +58,3 @@ jobs:
- Update SAS Strings from matrix-doc. - Update SAS Strings from matrix-doc.
branch: sync-sas-strings branch: sync-sas-strings
base: develop base: develop
sync-analytics-plan:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'element-hq/element-android'
# No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@v3
- name: Run analytics import script
run: ./tools/import_analytic_plan.sh
- name: Create Pull Request for analytics plan
uses: peter-evans/create-pull-request@v5
with:
commit-message: Sync analytics plan
title: Sync analytics plan
body: |
### Update analytics plan
Reviewers:
- [ ] Please remove usage of Event or Enum which may have been removed or updated
- [ ] please ensure new Events or new Enums are used to send analytics by pushing new commit(s) to this PR.
*Note*: Change are coming from [this project](https://github.com/matrix-org/matrix-analytics-events)
branch: sync-analytics-plan
base: develop

View file

@ -1,3 +1,18 @@
Changes in Element v1.6.16 (2024-05-29)
=======================================
Bugfixes 🐛
----------
- Fix crash when accessing a local file and permission is revoked. ([#3616](https://github.com/element-hq/element-android/issues/3616))
- Fixes Element on Android 12+ being ineligible for URL deeplinks ([#5748](https://github.com/element-hq/element-android/issues/5748))
- Restore formatting when restoring a draft. Also keep formatting when switching composer mode. ([#7466](https://github.com/element-hq/element-android/issues/7466))
Other changes
-------------
- Update posthog sdk to 3.2.0 ([#8820](https://github.com/element-hq/element-android/issues/8820))
- Update Rust crypto SDK to version 0.4.1 ([#8838](https://github.com/element-hq/element-android/issues/8838))
Changes in Element v1.6.14 (2024-04-02) Changes in Element v1.6.14 (2024-04-02)
======================================= =======================================

View file

@ -9,6 +9,7 @@ ext.groups = [
'com.github.hyuwah', 'com.github.hyuwah',
'com.github.jetradarmobile', 'com.github.jetradarmobile',
'com.github.MatrixFrog', 'com.github.MatrixFrog',
'com.github.matrix-org',
'com.github.SchildiChat', 'com.github.SchildiChat',
'com.github.tapadoo', 'com.github.tapadoo',
'com.github.UnifiedPush', 'com.github.UnifiedPush',
@ -121,7 +122,7 @@ ext.groups = [
'com.parse.bolts', 'com.parse.bolts',
'com.pinterest', 'com.pinterest',
'com.pinterest.ktlint', 'com.pinterest.ktlint',
'com.posthog.android', 'com.posthog',
'com.squareup', 'com.squareup',
'com.squareup.curtains', 'com.squareup.curtains',
'com.squareup.duktape', 'com.squareup.duktape',

View file

@ -17,7 +17,7 @@ We ask for the user to give consent before sending any analytics data.
The analytics plan is shared between all Element clients. To add an Event, please open a PR to this project: https://github.com/matrix-org/matrix-analytics-events The analytics plan is shared between all Element clients. To add an Event, please open a PR to this project: https://github.com/matrix-org/matrix-analytics-events
Then, once the PR has been merged, you can run the tool `import_analytic_plan.sh` to import the plan to Element, and then you can use the new Event. Note that this tool is run by Github action once a week. Then, once the PR has been merged, and the library is release, you can update the version of the library in the `build.gradle` file.
## Forks of Element ## Forks of Element

View file

@ -0,0 +1,2 @@
Main changes in this version: Bug fixes.
Full changelog: https://github.com/element-hq/element-android/releases

View file

@ -31,7 +31,7 @@ class AudioPicker : Picker<MultiPickerAudioType>() {
* Returns selected audio files or empty list if user did not select any files. * Returns selected audio files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerAudioType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerAudioType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerAudioType(context) selectedUri.toMultiPickerAudioType(context)
} }
} }

View file

@ -41,7 +41,7 @@ class FilePicker : Picker<MultiPickerBaseType>() {
* Returns selected files or empty list if user did not select any files. * Returns selected files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
val type = context.contentResolver.getType(selectedUri) val type = context.contentResolver.getType(selectedUri)
when { when {

View file

@ -31,7 +31,7 @@ class ImagePicker : Picker<MultiPickerImageType>() {
* Returns selected image files or empty list if user did not select any files. * Returns selected image files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerImageType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerImageType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerImageType(context) selectedUri.toMultiPickerImageType(context)
} }
} }

View file

@ -33,7 +33,7 @@ class MediaPicker : Picker<MultiPickerBaseMediaType>() {
* Returns selected image/video files or empty list if user did not select any files. * Returns selected image/video files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseMediaType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerBaseMediaType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
val mimeType = context.contentResolver.getType(selectedUri) val mimeType = context.contentResolver.getType(selectedUri)
if (mimeType.isMimeTypeVideo()) { if (mimeType.isMimeTypeVideo()) {

View file

@ -16,6 +16,7 @@
package im.vector.lib.multipicker package im.vector.lib.multipicker
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -58,7 +59,17 @@ abstract class Picker<T> {
uriList.forEach { uriList.forEach {
for (resolveInfo in resInfoList) { for (resolveInfo in resInfoList) {
val packageName: String = resolveInfo.activityInfo.packageName val packageName: String = resolveInfo.activityInfo.packageName
context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
// Replace implicit intent by an explicit to fix crash on some devices like Xiaomi.
// see https://juejin.cn/post/7031736325422186510
try {
context.grantUriPermission(packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
} catch (e: Exception) {
continue
}
data.action = null
data.component = ComponentName(packageName, resolveInfo.activityInfo.name)
break
} }
} }
return getSelectedFiles(context, data) return getSelectedFiles(context, data)
@ -82,7 +93,7 @@ abstract class Picker<T> {
activityResultLauncher.launch(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }) activityResultLauncher.launch(createIntent().apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) })
} }
protected fun getSelectedUriList(data: Intent?): List<Uri> { protected fun getSelectedUriList(context: Context, data: Intent?): List<Uri> {
val selectedUriList = mutableListOf<Uri>() val selectedUriList = mutableListOf<Uri>()
val dataUri = data?.data val dataUri = data?.data
val clipData = data?.clipData val clipData = data?.clipData
@ -104,6 +115,6 @@ abstract class Picker<T> {
} }
} }
} }
return selectedUriList return selectedUriList.onEach { context.grantUriPermission(context.applicationContext.packageName, it, Intent.FLAG_GRANT_READ_URI_PERMISSION) }
} }
} }

View file

@ -31,7 +31,7 @@ class VideoPicker : Picker<MultiPickerVideoType>() {
* Returns selected video files or empty list if user did not select any files. * Returns selected video files or empty list if user did not select any files.
*/ */
override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerVideoType> { override fun getSelectedFiles(context: Context, data: Intent?): List<MultiPickerVideoType> {
return getSelectedUriList(data).mapNotNull { selectedUri -> return getSelectedUriList(context, data).mapNotNull { selectedUri ->
selectedUri.toMultiPickerVideoType(context) selectedUri.toMultiPickerVideoType(context)
} }
} }

View file

@ -62,7 +62,7 @@ android {
// that the app's state is completely cleared between tests. // that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
buildConfigField "String", "SDK_VERSION", "\"1.6.14\"" buildConfigField "String", "SDK_VERSION", "\"1.6.16\""
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
@ -215,7 +215,7 @@ dependencies {
implementation libs.google.phonenumber implementation libs.google.phonenumber
implementation("org.matrix.rustcomponents:crypto-android:0.3.16") implementation("org.matrix.rustcomponents:crypto-android:0.4.1")
// api project(":library:rustCrypto") // api project(":library:rustCrypto")
testImplementation libs.tests.junit testImplementation libs.tests.junit

View file

@ -37,6 +37,10 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.util.MatrixJsonParser import org.matrix.android.sdk.api.util.MatrixJsonParser
import timber.log.Timber import timber.log.Timber
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO define an end-of-life date for this implementation.
/** /**
* Implementation of MSC3906 to sign in + E2EE set up using a QR code. * Implementation of MSC3906 to sign in + E2EE set up using a QR code.
*/ */

View file

@ -31,8 +31,8 @@ import org.matrix.android.sdk.internal.crypto.verification.SasVerification
import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest import org.matrix.android.sdk.internal.crypto.verification.VerificationRequest
import org.matrix.android.sdk.internal.crypto.verification.prepareMethods import org.matrix.android.sdk.internal.crypto.verification.prepareMethods
import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException
import org.matrix.rustcomponents.sdk.crypto.LocalTrust
import org.matrix.rustcomponents.sdk.crypto.SignatureException import org.matrix.rustcomponents.sdk.crypto.SignatureException
import uniffi.matrix_sdk_crypto.LocalTrust
import org.matrix.rustcomponents.sdk.crypto.Device as InnerDevice import org.matrix.rustcomponents.sdk.crypto.Device as InnerDevice
/** Class representing a device that supports E2EE in the Matrix world /** Class representing a device that supports E2EE in the Matrix world

View file

@ -75,7 +75,6 @@ import org.matrix.rustcomponents.sdk.crypto.DeviceLists
import org.matrix.rustcomponents.sdk.crypto.EncryptionSettings import org.matrix.rustcomponents.sdk.crypto.EncryptionSettings
import org.matrix.rustcomponents.sdk.crypto.KeyRequestPair import org.matrix.rustcomponents.sdk.crypto.KeyRequestPair
import org.matrix.rustcomponents.sdk.crypto.KeysImportResult import org.matrix.rustcomponents.sdk.crypto.KeysImportResult
import org.matrix.rustcomponents.sdk.crypto.LocalTrust
import org.matrix.rustcomponents.sdk.crypto.Logger import org.matrix.rustcomponents.sdk.crypto.Logger
import org.matrix.rustcomponents.sdk.crypto.MegolmV1BackupKey import org.matrix.rustcomponents.sdk.crypto.MegolmV1BackupKey
import org.matrix.rustcomponents.sdk.crypto.Request import org.matrix.rustcomponents.sdk.crypto.Request
@ -86,6 +85,7 @@ import org.matrix.rustcomponents.sdk.crypto.ShieldState
import org.matrix.rustcomponents.sdk.crypto.SignatureVerification import org.matrix.rustcomponents.sdk.crypto.SignatureVerification
import org.matrix.rustcomponents.sdk.crypto.setLogger import org.matrix.rustcomponents.sdk.crypto.setLogger
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk_crypto.LocalTrust
import java.io.File import java.io.File
import java.nio.charset.Charset import java.nio.charset.Charset
import javax.inject.Inject import javax.inject.Inject
@ -828,8 +828,14 @@ internal class OlmMachine @Inject constructor(
val requests = withContext(coroutineDispatchers.io) { val requests = withContext(coroutineDispatchers.io) {
inner.bootstrapCrossSigning() inner.bootstrapCrossSigning()
} }
(requests.uploadKeysRequest)?.let {
when (it) {
is Request.KeysUpload -> requestSender.uploadKeys(it)
else -> {}
}
}
requestSender.uploadCrossSigningKeys(requests.uploadSigningKeysRequest, uiaInterceptor) requestSender.uploadCrossSigningKeys(requests.uploadSigningKeysRequest, uiaInterceptor)
requestSender.sendSignatureUpload(requests.signatureRequest) requestSender.sendSignatureUpload(requests.uploadSignatureRequest)
} }
/** /**

View file

@ -68,9 +68,9 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import org.matrix.rustcomponents.sdk.crypto.Request import org.matrix.rustcomponents.sdk.crypto.Request
import org.matrix.rustcomponents.sdk.crypto.RequestType import org.matrix.rustcomponents.sdk.crypto.RequestType
import org.matrix.rustcomponents.sdk.crypto.SignatureState
import org.matrix.rustcomponents.sdk.crypto.SignatureVerification import org.matrix.rustcomponents.sdk.crypto.SignatureVerification
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk_crypto.SignatureState
import java.security.InvalidParameterException import java.security.InvalidParameterException
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random import kotlin.random.Random

View file

@ -100,7 +100,7 @@ fun RealmToMigrate.getPickledAccount(pickleKey: ByteArray): MigrationData {
) )
MigrationData( MigrationData(
account = pickledAccount, account = pickledAccount,
pickleKey = pickleKey.map { it.toUByte() }, pickleKey = pickleKey,
crossSigning = CrossSigningKeyExport( crossSigning = CrossSigningKeyExport(
masterKey = masterKey, masterKey = masterKey,
selfSigningKey = selfSignedKey, selfSigningKey = selfSignedKey,
@ -153,7 +153,7 @@ fun RealmToMigrate.getPickledAccount(pickleKey: ByteArray): MigrationData {
migrationData = MigrationData( migrationData = MigrationData(
account = pickledAccount, account = pickledAccount,
pickleKey = pickleKey.map { it.toUByte() }, pickleKey = pickleKey,
crossSigning = CrossSigningKeyExport( crossSigning = CrossSigningKeyExport(
masterKey = masterKey, masterKey = masterKey,
selfSigningKey = selfSignedKey, selfSigningKey = selfSignedKey,
@ -222,8 +222,10 @@ fun RealmToMigrate.pickledOlmSessions(pickleKey: ByteArray, chunkSize: Int, onCh
pickle = pickle, pickle = pickle,
senderKey = deviceKey, senderKey = deviceKey,
createdUsingFallbackKey = false, createdUsingFallbackKey = false,
creationTime = lastReceivedMessageTs.toString(), // / Unix timestamp (in seconds) when the session was created.
lastUseTime = lastReceivedMessageTs.toString() creationTime = (lastReceivedMessageTs / 1000).toULong(),
// / Unix timestamp (in seconds) when the session was last used.
lastUseTime = (lastReceivedMessageTs / 1000).toULong(),
) )
// should we check the tracking status? // should we check the tracking status?
pickledSessions.add(pickledSession) pickledSessions.add(pickledSession)
@ -323,8 +325,10 @@ private fun OlmSessionEntity.toPickledSession(pickleKey: ByteArray): PickledSess
pickle = pickledOlmSession, pickle = pickledOlmSession,
senderKey = deviceKey, senderKey = deviceKey,
createdUsingFallbackKey = false, createdUsingFallbackKey = false,
creationTime = lastReceivedMessageTs.toString(), // Rust expect in seconds
lastUseTime = lastReceivedMessageTs.toString() creationTime = (lastReceivedMessageTs / 1000).toULong(),
// Rust expect in seconds
lastUseTime = (lastReceivedMessageTs / 1000).toULong(),
) )
} }

View file

@ -17,8 +17,10 @@
package org.matrix.android.sdk.internal.session.content package org.matrix.android.sdk.internal.session.content
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.os.Build
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -115,7 +117,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
if (allCancelled) { if (allCancelled) {
// there is no point in uploading the image! // there is no point in uploading the image!
return Result.success(inputData) return Result.success(inputData)
.also { Timber.e("## Send: Work cancelled by user") } .also {
Timber.e("## Send: Work cancelled by user")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.revokeUriPermission(context.packageName, params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
context.revokeUriPermission(params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
} }
val attachment = params.attachment val attachment = params.attachment
@ -399,6 +409,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
) )
return Result.success(WorkerParamsFactory.toData(sendParams)).also { return Result.success(WorkerParamsFactory.toData(sendParams)).also {
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.revokeUriPermission(context.packageName, params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
context.revokeUriPermission(params.attachment.queryUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
} }
} }

View file

@ -2638,18 +2638,20 @@
"a": "⊛ Head Shaking Horizontally", "a": "⊛ Head Shaking Horizontally",
"b": "1F642-200D-2194-FE0F", "b": "1F642-200D-2194-FE0F",
"j": [ "j": [
"head shaking horizontally",
"no", "no",
"shake" "shake",
"disapprove",
"indiffernt",
"left"
] ]
}, },
"head-shaking-vertically": { "head-shaking-vertically": {
"a": "⊛ Head Shaking Vertically", "a": "⊛ Head Shaking Vertically",
"b": "1F642-200D-2195-FE0F", "b": "1F642-200D-2195-FE0F",
"j": [ "j": [
"head shaking vertically",
"nod", "nod",
"yes" "yes",
"down"
] ]
}, },
"relieved-face": { "relieved-face": {
@ -6941,21 +6943,27 @@
"a": "⊛ Person Walking Facing Right", "a": "⊛ Person Walking Facing Right",
"b": "1F6B6-200D-27A1-FE0F", "b": "1F6B6-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"peerson",
"exercise"
] ]
}, },
"woman-walking-facing-right": { "woman-walking-facing-right": {
"a": "⊛ Woman Walking Facing Right", "a": "⊛ Woman Walking Facing Right",
"b": "1F6B6-200D-2640-FE0F-200D-27A1-FE0F", "b": "1F6B6-200D-2640-FE0F-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"person",
"exercise"
] ]
}, },
"man-walking-facing-right": { "man-walking-facing-right": {
"a": "⊛ Man Walking Facing Right", "a": "⊛ Man Walking Facing Right",
"b": "1F6B6-200D-2642-FE0F-200D-27A1-FE0F", "b": "1F6B6-200D-2642-FE0F-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"person",
"exercise"
] ]
}, },
"person-standing": { "person-standing": {
@ -7019,21 +7027,26 @@
"a": "⊛ Person Kneeling Facing Right", "a": "⊛ Person Kneeling Facing Right",
"b": "1F9CE-200D-27A1-FE0F", "b": "1F9CE-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"pray"
] ]
}, },
"woman-kneeling-facing-right": { "woman-kneeling-facing-right": {
"a": "⊛ Woman Kneeling Facing Right", "a": "⊛ Woman Kneeling Facing Right",
"b": "1F9CE-200D-2640-FE0F-200D-27A1-FE0F", "b": "1F9CE-200D-2640-FE0F-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"pray",
"worship"
] ]
}, },
"man-kneeling-facing-right": { "man-kneeling-facing-right": {
"a": "⊛ Man Kneeling Facing Right", "a": "⊛ Man Kneeling Facing Right",
"b": "1F9CE-200D-2642-FE0F-200D-27A1-FE0F", "b": "1F9CE-200D-2642-FE0F-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"pray",
"worship"
] ]
}, },
"person-with-white-cane": { "person-with-white-cane": {
@ -7049,7 +7062,10 @@
"a": "⊛ Person with White Cane Facing Right", "a": "⊛ Person with White Cane Facing Right",
"b": "1F9D1-200D-1F9AF-200D-27A1-FE0F", "b": "1F9D1-200D-1F9AF-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"walk",
"visually impaired",
"blind"
] ]
}, },
"man-with-white-cane": { "man-with-white-cane": {
@ -7066,7 +7082,11 @@
"a": "⊛ Man with White Cane Facing Right", "a": "⊛ Man with White Cane Facing Right",
"b": "1F468-200D-1F9AF-200D-27A1-FE0F", "b": "1F468-200D-1F9AF-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"visually impaired",
"blind",
"walk",
"stick"
] ]
}, },
"woman-with-white-cane": { "woman-with-white-cane": {
@ -7083,7 +7103,10 @@
"a": "⊛ Woman with White Cane Facing Right", "a": "⊛ Woman with White Cane Facing Right",
"b": "1F469-200D-1F9AF-200D-27A1-FE0F", "b": "1F469-200D-1F9AF-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"stick",
"visually impaired",
"blind"
] ]
}, },
"person-in-motorized-wheelchair": { "person-in-motorized-wheelchair": {
@ -7099,7 +7122,9 @@
"a": "⊛ Person in Motorized Wheelchair Facing Right", "a": "⊛ Person in Motorized Wheelchair Facing Right",
"b": "1F9D1-200D-1F9BC-200D-27A1-FE0F", "b": "1F9D1-200D-1F9BC-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"accessibility",
"disability"
] ]
}, },
"man-in-motorized-wheelchair": { "man-in-motorized-wheelchair": {
@ -7116,7 +7141,10 @@
"a": "⊛ Man in Motorized Wheelchair Facing Right", "a": "⊛ Man in Motorized Wheelchair Facing Right",
"b": "1F468-200D-1F9BC-200D-27A1-FE0F", "b": "1F468-200D-1F9BC-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"disability",
"accessibility",
"mobility"
] ]
}, },
"woman-in-motorized-wheelchair": { "woman-in-motorized-wheelchair": {
@ -7133,7 +7161,10 @@
"a": "⊛ Woman in Motorized Wheelchair Facing Right", "a": "⊛ Woman in Motorized Wheelchair Facing Right",
"b": "1F469-200D-1F9BC-200D-27A1-FE0F", "b": "1F469-200D-1F9BC-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"mobility",
"accessibility",
"disability"
] ]
}, },
"person-in-manual-wheelchair": { "person-in-manual-wheelchair": {
@ -7149,7 +7180,10 @@
"a": "⊛ Person in Manual Wheelchair Facing Right", "a": "⊛ Person in Manual Wheelchair Facing Right",
"b": "1F9D1-200D-1F9BD-200D-27A1-FE0F", "b": "1F9D1-200D-1F9BD-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"mobility",
"accessibility",
"disability"
] ]
}, },
"man-in-manual-wheelchair": { "man-in-manual-wheelchair": {
@ -7166,7 +7200,10 @@
"a": "⊛ Man in Manual Wheelchair Facing Right", "a": "⊛ Man in Manual Wheelchair Facing Right",
"b": "1F468-200D-1F9BD-200D-27A1-FE0F", "b": "1F468-200D-1F9BD-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"mobility",
"accessibility",
"disability"
] ]
}, },
"woman-in-manual-wheelchair": { "woman-in-manual-wheelchair": {
@ -7183,7 +7220,10 @@
"a": "⊛ Woman in Manual Wheelchair Facing Right", "a": "⊛ Woman in Manual Wheelchair Facing Right",
"b": "1F469-200D-1F9BD-200D-27A1-FE0F", "b": "1F469-200D-1F9BD-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"disability",
"mobility",
"accessibility"
] ]
}, },
"person-running": { "person-running": {
@ -7226,21 +7266,27 @@
"a": "⊛ Person Running Facing Right", "a": "⊛ Person Running Facing Right",
"b": "1F3C3-200D-27A1-FE0F", "b": "1F3C3-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"exercise",
"jog"
] ]
}, },
"woman-running-facing-right": { "woman-running-facing-right": {
"a": "⊛ Woman Running Facing Right", "a": "⊛ Woman Running Facing Right",
"b": "1F3C3-200D-2640-FE0F-200D-27A1-FE0F", "b": "1F3C3-200D-2640-FE0F-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"exercise",
"jog"
] ]
}, },
"man-running-facing-right": { "man-running-facing-right": {
"a": "⊛ Man Running Facing Right", "a": "⊛ Man Running Facing Right",
"b": "1F3C3-200D-2642-FE0F-200D-27A1-FE0F", "b": "1F3C3-200D-2642-FE0F-200D-27A1-FE0F",
"j": [ "j": [
"" "",
"jog",
"exercise"
] ]
}, },
"woman-dancing": { "woman-dancing": {
@ -8538,28 +8584,40 @@
"a": "⊛ Family: Adult, Adult, Child", "a": "⊛ Family: Adult, Adult, Child",
"b": "1F9D1-200D-1F9D1-200D-1F9D2", "b": "1F9D1-200D-1F9D1-200D-1F9D2",
"j": [ "j": [
"family: adult, adult, child" "family: adult, adult, child",
"family adult, adult, child",
"kid",
"parents"
] ]
}, },
"family-adult-adult-child-child": { "family-adult-adult-child-child": {
"a": "⊛ Family: Adult, Adult, Child, Child", "a": "⊛ Family: Adult, Adult, Child, Child",
"b": "1F9D1-200D-1F9D1-200D-1F9D2-200D-1F9D2", "b": "1F9D1-200D-1F9D1-200D-1F9D2-200D-1F9D2",
"j": [ "j": [
"family: adult, adult, child, child" "family: adult, adult, child, child",
"family adult, adult, child, child",
"children",
"parents"
] ]
}, },
"family-adult-child": { "family-adult-child": {
"a": "⊛ Family: Adult, Child", "a": "⊛ Family: Adult, Child",
"b": "1F9D1-200D-1F9D2", "b": "1F9D1-200D-1F9D2",
"j": [ "j": [
"family: adult, child" "family: adult, child",
"family adult, child",
"parent",
"kid"
] ]
}, },
"family-adult-child-child": { "family-adult-child-child": {
"a": "⊛ Family: Adult, Child, Child", "a": "⊛ Family: Adult, Child, Child",
"b": "1F9D1-200D-1F9D2-200D-1F9D2", "b": "1F9D1-200D-1F9D2-200D-1F9D2",
"j": [ "j": [
"family: adult, child, child" "family: adult, child, child",
"family adult, child, child",
"parent",
"children"
] ]
}, },
"footprints": { "footprints": {
@ -9564,9 +9622,12 @@
"j": [ "j": [
"fantasy", "fantasy",
"firebird", "firebird",
"phoenix",
"rebirth", "rebirth",
"reincarnation" "reincarnation",
"immortal",
"bird",
"mythtical",
"reborn"
] ]
}, },
"frog": { "frog": {
@ -10364,8 +10425,9 @@
"j": [ "j": [
"citrus", "citrus",
"fruit", "fruit",
"lime", "tropical",
"tropical" "acidic",
"citric"
] ]
}, },
"banana": { "banana": {
@ -10689,11 +10751,11 @@
"a": "⊛ Brown Mushroom", "a": "⊛ Brown Mushroom",
"b": "1F344-200D-1F7EB", "b": "1F344-200D-1F7EB",
"j": [ "j": [
"brown mushroom",
"food", "food",
"fungus", "fungus",
"nature", "nature",
"vegetable" "vegetable",
"toadstool"
] ]
}, },
"bread": { "bread": {
@ -17986,10 +18048,10 @@
"j": [ "j": [
"break", "break",
"breaking", "breaking",
"broken chain",
"chain", "chain",
"cuffs", "cuffs",
"freedom" "freedom",
"constraint"
] ]
}, },
"chains": { "chains": {

View file

@ -1,18 +0,0 @@
#!/usr/bin/env bash
echo "Deleted existing plan..."
rm vector/src/main/java/im/vector/app/features/analytics/plan/*.*
echo "Cloning analytics project..."
mkdir analytics_tmp
cd analytics_tmp
git clone https://github.com/matrix-org/matrix-analytics-events.git
echo "Copy plan..."
cp matrix-analytics-events/types/kotlin2/* ../vector/src/main/java/im/vector/app/features/analytics/plan/
echo "Cleanup."
cd ..
rm -rf analytics_tmp
echo "Done."

View file

@ -37,7 +37,7 @@ ext.versionMinor = 6
// Note: even values are reserved for regular release, odd values for hotfix release. // Note: even values are reserved for regular release, odd values for hotfix release.
// When creating a hotfix, you should decrease the value, since the current value // When creating a hotfix, you should decrease the value, since the current value
// is the value for the next regular release. // is the value for the next regular release.
ext.versionPatch = 14 ext.versionPatch = 16
ext.scVersion = 77 ext.scVersion = 77

View file

@ -161,6 +161,9 @@ dependencies {
// Debug // Debug
api 'com.facebook.stetho:stetho:1.6.0' api 'com.facebook.stetho:stetho:1.6.0'
// Analytics
api 'com.github.matrix-org:matrix-analytics-events:0.15.0'
api libs.google.phonenumber api libs.google.phonenumber
// FlowBinding // FlowBinding
@ -233,9 +236,7 @@ dependencies {
kapt libs.dagger.hiltCompiler kapt libs.dagger.hiltCompiler
// Analytics // Analytics
implementation('com.posthog.android:posthog:2.0.3') { implementation 'com.posthog:posthog-android:3.2.0'
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation libs.sentry.sentryAndroid implementation libs.sentry.sentryAndroid
// UnifiedPush // UnifiedPush

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.InstrumentedTest
import im.vector.app.features.analytics.ReportedDecryptionFailurePersistence
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReportedDecryptionFailurePersistenceTest : InstrumentedTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
@Test
fun shouldPersistReportedUtds() = runTest {
val persistence = ReportedDecryptionFailurePersistence(context)
persistence.load()
val eventIds = listOf("$0000", "$0001", "$0002", "$0003")
eventIds.forEach {
persistence.markAsReported(it)
}
eventIds.forEach {
persistence.hasBeenReported(it) shouldBeEqualTo true
}
persistence.hasBeenReported("$0004") shouldBeEqualTo false
persistence.persist()
// Load a new one
val persistence2 = ReportedDecryptionFailurePersistence(context)
persistence2.load()
eventIds.forEach {
persistence2.hasBeenReported(it) shouldBeEqualTo true
}
}
@Test
fun testSaturation() = runTest {
val persistence = ReportedDecryptionFailurePersistence(context)
for (i in 1..6000) {
persistence.markAsReported("000$i")
}
// This should have saturated the bloom filter, making the rate of false positives too high.
// A new bloom filter should have been created to avoid that and the recent reported events should still be in the new filter.
for (i in 5800..6000) {
persistence.hasBeenReported("000$i") shouldBeEqualTo true
}
// Old ones should not be there though
for (i in 1..1000) {
persistence.hasBeenReported("000$i") shouldBeEqualTo false
}
}
}

View file

@ -178,10 +178,15 @@
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="riot.im" /> <data android:host="riot.im" />
<data android:host="app.element.io" /> </intent-filter>
<data android:host="mobile.element.io" /> <intent-filter android:autoVerify="true">
<data android:host="develop.element.io" /> <action android:name="android.intent.action.VIEW" />
<data android:host="staging.element.io" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="*.element.io" />
</intent-filter> </intent-filter>
</activity> </activity>

View file

@ -63,6 +63,7 @@ private const val MAX_WAIT_MILLIS = 60_000
class DecryptionFailureTracker @Inject constructor( class DecryptionFailureTracker @Inject constructor(
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val sessionDataSource: ActiveSessionDataSource, private val sessionDataSource: ActiveSessionDataSource,
private val decryptionFailurePersistence: ReportedDecryptionFailurePersistence,
private val clock: Clock private val clock: Clock
) : Session.Listener, LiveEventListener { ) : Session.Listener, LiveEventListener {
@ -76,9 +77,6 @@ class DecryptionFailureTracker @Inject constructor(
// Only accessed on a `post` call, ensuring sequential access // Only accessed on a `post` call, ensuring sequential access
private val trackedEventsMap = mutableMapOf<String, DecryptionFailure>() private val trackedEventsMap = mutableMapOf<String, DecryptionFailure>()
// List of eventId that have been reported, to avoid double reporting
private val alreadyReported = mutableListOf<String>()
// Mutex to ensure sequential access to internal state // Mutex to ensure sequential access to internal state
private val mutex = Mutex() private val mutex = Mutex()
@ -98,10 +96,16 @@ class DecryptionFailureTracker @Inject constructor(
this.scope = scope this.scope = scope
} }
observeActiveSession() observeActiveSession()
post {
decryptionFailurePersistence.load()
}
} }
fun stop() { fun stop() {
Timber.v("Stop DecryptionFailureTracker") Timber.v("Stop DecryptionFailureTracker")
post {
decryptionFailurePersistence.persist()
}
activeSessionSourceDisposable.cancel(CancellationException("Closing DecryptionFailureTracker")) activeSessionSourceDisposable.cancel(CancellationException("Closing DecryptionFailureTracker"))
activeSession?.removeListener(this) activeSession?.removeListener(this)
@ -123,6 +127,7 @@ class DecryptionFailureTracker @Inject constructor(
delay(CHECK_INTERVAL) delay(CHECK_INTERVAL)
post { post {
checkFailures() checkFailures()
decryptionFailurePersistence.persist()
currentTicker = null currentTicker = null
if (trackedEventsMap.isNotEmpty()) { if (trackedEventsMap.isNotEmpty()) {
// Reschedule // Reschedule
@ -136,7 +141,7 @@ class DecryptionFailureTracker @Inject constructor(
.distinctUntilChanged() .distinctUntilChanged()
.onEach { .onEach {
Timber.v("Active session changed ${it.getOrNull()?.myUserId}") Timber.v("Active session changed ${it.getOrNull()?.myUserId}")
it.orNull()?.let { session -> it.getOrNull()?.let { session ->
post { post {
onSessionActive(session) onSessionActive(session)
} }
@ -144,7 +149,7 @@ class DecryptionFailureTracker @Inject constructor(
}.launchIn(scope) }.launchIn(scope)
} }
private fun onSessionActive(session: Session) { private suspend fun onSessionActive(session: Session) {
Timber.v("onSessionActive ${session.myUserId} previous: ${activeSession?.myUserId}") Timber.v("onSessionActive ${session.myUserId} previous: ${activeSession?.myUserId}")
val sessionId = session.sessionId val sessionId = session.sessionId
if (sessionId == activeSession?.sessionId) { if (sessionId == activeSession?.sessionId) {
@ -201,7 +206,8 @@ class DecryptionFailureTracker @Inject constructor(
// already tracked // already tracked
return return
} }
if (alreadyReported.contains(eventId)) { if (decryptionFailurePersistence.hasBeenReported(eventId)) {
Timber.v("Event $eventId already reported")
// already reported // already reported
return return
} }
@ -236,7 +242,7 @@ class DecryptionFailureTracker @Inject constructor(
} }
} }
private fun handleEventDecrypted(eventId: String) { private suspend fun handleEventDecrypted(eventId: String) {
Timber.v("Handle event decrypted $eventId time: ${clock.epochMillis()}") Timber.v("Handle event decrypted $eventId time: ${clock.epochMillis()}")
// Only consider if it was tracked as a failure // Only consider if it was tracked as a failure
val trackedFailure = trackedEventsMap[eventId] ?: return val trackedFailure = trackedEventsMap[eventId] ?: return
@ -269,7 +275,7 @@ class DecryptionFailureTracker @Inject constructor(
} }
// This will mutate the trackedEventsMap, so don't call it while iterating on it. // This will mutate the trackedEventsMap, so don't call it while iterating on it.
private fun reportFailure(decryptionFailure: DecryptionFailure) { private suspend fun reportFailure(decryptionFailure: DecryptionFailure) {
Timber.v("Report failure for event ${decryptionFailure.failedEventId}") Timber.v("Report failure for event ${decryptionFailure.failedEventId}")
val error = decryptionFailure.toAnalyticsEvent() val error = decryptionFailure.toAnalyticsEvent()
@ -278,10 +284,10 @@ class DecryptionFailureTracker @Inject constructor(
// now remove from tracked // now remove from tracked
trackedEventsMap.remove(decryptionFailure.failedEventId) trackedEventsMap.remove(decryptionFailure.failedEventId)
// mark as already reported // mark as already reported
alreadyReported.add(decryptionFailure.failedEventId) decryptionFailurePersistence.markAsReported(decryptionFailure.failedEventId)
} }
private fun checkFailures() { private suspend fun checkFailures() {
val now = clock.epochMillis() val now = clock.epochMillis()
Timber.v("Check failures now $now") Timber.v("Check failures now $now")
// report the definitely failed // report the definitely failed

View file

@ -0,0 +1,122 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics
import android.content.Context
import android.util.LruCache
import com.google.common.hash.BloomFilter
import com.google.common.hash.Funnels
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import javax.inject.Inject
private const val REPORTED_UTD_FILE_NAME = "im.vector.analytics.reported_utd"
private const val EXPECTED_INSERTIONS = 5000
/**
* This class is used to keep track of the reported decryption failures to avoid double reporting.
* It uses a bloom filter to limit the memory/disk usage.
*/
class ReportedDecryptionFailurePersistence @Inject constructor(
private val context: Context,
) {
// Keep a cache of recent reported failures in memory.
// They will be persisted to the a new bloom filter if the previous one is getting saturated.
// Should be around 30KB max in memory.
// Also allows to have 0% false positive rate for recent failures.
private val inMemoryReportedFailures: LruCache<String, Unit> = LruCache(300)
// Thread-safe and lock-free.
// The expected insertions is 5000, and expected false positive probability of 3% when close to max capability.
// The persisted size is expected to be around 5KB (100 times less than if it was raw strings).
private var bloomFilter: BloomFilter<String> = BloomFilter.create<String>(Funnels.stringFunnel(Charsets.UTF_8), EXPECTED_INSERTIONS)
/**
* Mark an event as reported.
* @param eventId the event id to mark as reported.
*/
suspend fun markAsReported(eventId: String) {
// Add to in memory cache.
inMemoryReportedFailures.put(eventId, Unit)
bloomFilter.put(eventId)
// check if the filter is getting saturated? and then replace
if (bloomFilter.approximateElementCount() > EXPECTED_INSERTIONS - 500) {
// The filter is getting saturated, and the false positive rate is increasing.
// It's time to replace the filter with a new one. And move the in-memory cache to the new filter.
bloomFilter = BloomFilter.create<String>(Funnels.stringFunnel(Charsets.UTF_8), EXPECTED_INSERTIONS)
inMemoryReportedFailures.snapshot().keys.forEach {
bloomFilter.put(it)
}
persist()
}
Timber.v("## Bloom filter stats: expectedFpp: ${bloomFilter.expectedFpp()}, size: ${bloomFilter.approximateElementCount()}")
}
/**
* Check if an event has been reported.
* @param eventId the event id to check.
* @return true if the event has been reported.
*/
fun hasBeenReported(eventId: String): Boolean {
// First check in memory cache.
if (inMemoryReportedFailures.get(eventId) != null) {
return true
}
return bloomFilter.mightContain(eventId)
}
/**
* Load the reported failures from disk.
*/
suspend fun load() {
withContext(Dispatchers.IO) {
try {
val file = File(context.applicationContext.cacheDir, REPORTED_UTD_FILE_NAME)
if (file.exists()) {
file.inputStream().use {
bloomFilter = BloomFilter.readFrom(it, Funnels.stringFunnel(Charsets.UTF_8))
}
}
} catch (e: Throwable) {
Timber.e(e, "## Failed to load reported failures")
}
}
}
/**
* Persist the reported failures to disk.
*/
suspend fun persist() {
withContext(Dispatchers.IO) {
try {
val file = File(context.applicationContext.cacheDir, REPORTED_UTD_FILE_NAME)
if (!file.exists()) file.createNewFile()
FileOutputStream(file).buffered().use {
bloomFilter.writeTo(it)
}
Timber.v("## Successfully saved reported failures, size: ${file.length()}")
} catch (e: Throwable) {
Timber.e(e, "## Failed to save reported failures")
}
}
}
}

View file

@ -16,9 +16,7 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import com.posthog.android.Options import com.posthog.PostHogInterface
import com.posthog.android.PostHog
import com.posthog.android.Properties
import im.vector.app.core.di.NamedGlobalScope import im.vector.app.core.di.NamedGlobalScope
import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
@ -36,9 +34,6 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
private val REUSE_EXISTING_ID: String? = null
private val IGNORED_OPTIONS: Options? = null
@Singleton @Singleton
class DefaultVectorAnalytics @Inject constructor( class DefaultVectorAnalytics @Inject constructor(
private val postHogFactory: PostHogFactory, private val postHogFactory: PostHogFactory,
@ -49,9 +44,9 @@ class DefaultVectorAnalytics @Inject constructor(
@NamedGlobalScope private val globalScope: CoroutineScope @NamedGlobalScope private val globalScope: CoroutineScope
) : VectorAnalytics { ) : VectorAnalytics {
private var posthog: PostHog? = null private var posthog: PostHogInterface? = null
private fun createPosthog(): PostHog? { private fun createPosthog(): PostHogInterface? {
return when { return when {
analyticsConfig.isEnabled -> postHogFactory.createPosthog() analyticsConfig.isEnabled -> postHogFactory.createPosthog()
else -> { else -> {
@ -126,7 +121,7 @@ class DefaultVectorAnalytics @Inject constructor(
posthog?.reset() posthog?.reset()
} else { } else {
Timber.tag(analyticsTag.value).d("identify") Timber.tag(analyticsTag.value).d("identify")
posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS) posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties())
} }
} }
@ -155,7 +150,7 @@ class DefaultVectorAnalytics @Inject constructor(
when (_userConsent) { when (_userConsent) {
true -> { true -> {
posthog = createPosthog() posthog = createPosthog()
posthog?.optOut(false) posthog?.optIn()
identifyPostHog() identifyPostHog()
pendingUserProperties?.let { doUpdateUserProperties(it) } pendingUserProperties?.let { doUpdateUserProperties(it) }
pendingUserProperties = null pendingUserProperties = null
@ -163,8 +158,8 @@ class DefaultVectorAnalytics @Inject constructor(
false -> { false -> {
// When opting out, ensure that the queue is flushed first, or it will be flushed later (after user has revoked consent) // When opting out, ensure that the queue is flushed first, or it will be flushed later (after user has revoked consent)
posthog?.flush() posthog?.flush()
posthog?.optOut(true) posthog?.optOut()
posthog?.shutdown() posthog?.close()
posthog = null posthog = null
} }
} }
@ -177,6 +172,7 @@ class DefaultVectorAnalytics @Inject constructor(
?.takeIf { userConsent == true } ?.takeIf { userConsent == true }
?.capture( ?.capture(
event.getName(), event.getName(),
analyticsId,
event.getProperties()?.toPostHogProperties() event.getProperties()?.toPostHogProperties()
) )
} }
@ -197,27 +193,37 @@ class DefaultVectorAnalytics @Inject constructor(
} }
private fun doUpdateUserProperties(userProperties: UserProperties) { private fun doUpdateUserProperties(userProperties: UserProperties) {
// we need a distinct id to set user properties
val distinctId = analyticsId ?: return
posthog posthog
?.takeIf { userConsent == true } ?.takeIf { userConsent == true }
?.identify(REUSE_EXISTING_ID, userProperties.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS) ?.identify(distinctId, userProperties.getProperties())
} }
private fun Map<String, Any?>?.toPostHogProperties(): Properties? { private fun Map<String, Any?>?.toPostHogProperties(): Map<String, Any>? {
if (this == null) return null if (this == null) return null
return Properties().apply { val nonNulls = HashMap<String, Any>()
putAll(this@toPostHogProperties) this.forEach { (key, value) ->
if (value != null) {
nonNulls[key] = value
}
} }
return nonNulls
} }
/** /**
* We avoid sending nulls as part of the UserProperties as this will reset the values across all devices. * We avoid sending nulls as part of the UserProperties as this will reset the values across all devices.
* The UserProperties event has nullable properties to allow for clients to opt in. * The UserProperties event has nullable properties to allow for clients to opt in.
*/ */
private fun Map<String, Any?>.toPostHogUserProperties(): Properties { private fun Map<String, Any?>.toPostHogUserProperties(): Map<String, Any> {
return Properties().apply { val nonNulls = HashMap<String, Any>()
putAll(this@toPostHogUserProperties.filter { it.value != null }) this.forEach { (key, value) ->
if (value != null) {
nonNulls[key] = value
}
} }
return nonNulls
} }
override fun trackError(throwable: Throwable) { override fun trackError(throwable: Throwable) {

View file

@ -17,7 +17,9 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import android.content.Context import android.content.Context
import com.posthog.android.PostHog import com.posthog.PostHogInterface
import com.posthog.android.PostHogAndroid
import com.posthog.android.PostHogAndroidConfig
import im.vector.app.core.resources.BuildMeta import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import javax.inject.Inject import javax.inject.Inject
@ -28,29 +30,17 @@ class PostHogFactory @Inject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) { ) {
fun createPosthog(): PostHog { fun createPosthog(): PostHogInterface {
return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost) val config = PostHogAndroidConfig(
// Record certain application events automatically! (off/false by default) apiKey = analyticsConfig.postHogApiKey,
// .captureApplicationLifecycleEvents() host = analyticsConfig.postHogHost,
// Record screen views automatically! (off/false by default) // we do that manually
// .recordScreenViews() captureScreenViews = false,
// Capture deep links as part of the screen call. (off by default) ).also {
// .captureDeepLinks() if (buildMeta.isDebug) {
// Maximum number of events to keep in queue before flushing (default 20) it.debug = true
// .flushQueueSize(20) }
// Max delay before flushing the queue (30 seconds)
// .flushInterval(30, TimeUnit.SECONDS)
// Enable or disable collection of ANDROID_ID (true)
.collectDeviceId(false)
.logLevel(getLogLevel())
.build()
}
private fun getLogLevel(): PostHog.LogLevel {
return if (buildMeta.isDebug) {
PostHog.LogLevel.DEBUG
} else {
PostHog.LogLevel.INFO
} }
return PostHogAndroid.with(context, config)
} }
} }

View file

@ -1,22 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.itf
interface VectorAnalyticsEvent {
fun getName(): String
fun getProperties(): Map<String, Any?>?
}

View file

@ -1,22 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.itf
interface VectorAnalyticsScreen {
fun getName(): String
fun getProperties(): Map<String, Any>?
}

View file

@ -1,56 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when a call has ended.
*/
data class CallEnded(
/**
* The duration of the call in milliseconds.
*/
val durationMs: Int,
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
) : VectorAnalyticsEvent {
override fun getName() = "CallEnded"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("durationMs", durationMs)
put("isVideo", isVideo)
put("numParticipants", numParticipants)
put("placed", placed)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,51 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when an error occurred in a call.
*/
data class CallError(
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
) : VectorAnalyticsEvent {
override fun getName() = "CallError"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isVideo", isVideo)
put("numParticipants", numParticipants)
put("placed", placed)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,51 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when a call is started.
*/
data class CallStarted(
/**
* Whether its a video call or not.
*/
val isVideo: Boolean,
/**
* Number of participants in the call.
*/
val numParticipants: Int,
/**
* Whether this user placed it.
*/
val placed: Boolean,
) : VectorAnalyticsEvent {
override fun getName() = "CallStarted"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isVideo", isVideo)
put("numParticipants", numParticipants)
put("placed", placed)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,90 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user sends a message via the composer.
*/
data class Composer(
/**
* Whether the user was using the composer inside of a thread.
*/
val inThread: Boolean,
/**
* Whether the user's composer interaction was editing a previously sent
* event.
*/
val isEditing: Boolean,
/**
* Whether the user's composer interaction was a reply to a previously
* sent event.
*/
val isReply: Boolean,
/**
* The type of the message.
*/
val messageType: MessageType,
/**
* Whether this message begins a new thread or not.
*/
val startsThread: Boolean? = null,
) : VectorAnalyticsEvent {
enum class MessageType {
/**
* A pin drop location message.
*/
LocationPin,
/**
* A user current location message.
*/
LocationUser,
/**
* A poll message.
*/
Poll,
/**
* A text message.
*/
Text,
/**
* A voice message.
*/
VoiceMessage,
}
override fun getName() = "Composer"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("inThread", inThread)
put("isEditing", isEditing)
put("isReply", isReply)
put("messageType", messageType.name)
startsThread?.let { put("startsThread", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,41 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user creates a room.
*/
data class CreatedRoom(
/**
* Whether the room is a DM.
*/
val isDM: Boolean,
) : VectorAnalyticsEvent {
override fun getName() = "CreatedRoom"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isDM", isDM)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,183 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when an error occurred.
*/
data class Error(
/**
* Context - client defined, can be used for debugging.
*/
val context: String? = null,
/**
* DEPRECATED: Which crypto module is the client currently using.
*/
val cryptoModule: CryptoModule? = null,
/**
* Which crypto backend is the client currently using.
*/
val cryptoSDK: CryptoSDK? = null,
val domain: Domain,
/**
* An heuristic based on event origin_server_ts and the current device
* creation time (origin_server_ts - device_ts). This would be used to
* get the source of the event scroll-back/live/initialSync.
*/
val eventLocalAgeMillis: Int? = null,
/**
* true if userDomain != senderDomain.
*/
val isFederated: Boolean? = null,
/**
* true if the current user is using matrix.org.
*/
val isMatrixDotOrg: Boolean? = null,
val name: Name,
/**
* UTDs can be permanent or temporary. If temporary, this field will
* contain the time it took to decrypt the message in milliseconds. If
* permanent should be -1.
*/
val timeToDecryptMillis: Int? = null,
/**
* true if the current user trusts their own identity (verified session)
* at time of decryption.
*/
val userTrustsOwnIdentity: Boolean? = null,
/**
* true if that unable to decrypt error was visible to the user.
*/
val wasVisibleToUser: Boolean? = null,
) : VectorAnalyticsEvent {
enum class Domain {
E2EE,
TO_DEVICE,
VOIP,
}
enum class Name {
/**
* E2EE domain error. Decryption failed for a message sent before the
* device logged in, and key backup is not enabled.
*/
HistoricalMessage,
/**
* E2EE domain error. The room key is known but is ratcheted (index >
* 0).
*/
OlmIndexError,
/**
* E2EE domain error. Generic unknown inbound group session error.
*/
OlmKeysNotSentError,
/**
* E2EE domain error. Any other decryption error (missing field, format
* errors...).
*/
OlmUnspecifiedError,
/**
* TO_DEVICE domain error. The to-device message failed to decrypt.
*/
ToDeviceFailedToDecrypt,
/**
* E2EE domain error. Decryption failed due to unknown error.
*/
UnknownError,
/**
* VOIP domain error. ICE negotiation failed.
*/
VoipIceFailed,
/**
* VOIP domain error. ICE negotiation timed out.
*/
VoipIceTimeout,
/**
* VOIP domain error. The call invite timed out.
*/
VoipInviteTimeout,
/**
* VOIP domain error. The user hung up the call.
*/
VoipUserHangup,
/**
* VOIP domain error. The user's media failed to start.
*/
VoipUserMediaFailed,
}
enum class CryptoSDK {
/**
* Legacy crypto backend specific to each platform.
*/
Legacy,
/**
* Cross-platform crypto backend written in Rust.
*/
Rust,
}
enum class CryptoModule {
/**
* Native / legacy crypto module specific to each platform.
*/
Native,
/**
* Shared / cross-platform crypto module written in Rust.
*/
Rust,
}
override fun getName() = "Error"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
context?.let { put("context", it) }
cryptoModule?.let { put("cryptoModule", it.name) }
cryptoSDK?.let { put("cryptoSDK", it.name) }
put("domain", domain.name)
eventLocalAgeMillis?.let { put("eventLocalAgeMillis", it) }
isFederated?.let { put("isFederated", it) }
isMatrixDotOrg?.let { put("isMatrixDotOrg", it) }
put("name", name.name)
timeToDecryptMillis?.let { put("timeToDecryptMillis", it) }
userTrustsOwnIdentity?.let { put("userTrustsOwnIdentity", it) }
wasVisibleToUser?.let { put("wasVisibleToUser", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,509 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user clicks/taps/activates a UI element.
*/
data class Interaction(
/**
* The index of the element, if its in a list of elements.
*/
val index: Int? = null,
/**
* The manner with which the user activated the UI element.
*/
val interactionType: InteractionType? = null,
/**
* The unique name of this element.
*/
val name: Name,
) : VectorAnalyticsEvent {
enum class Name {
/**
* User tapped the All filter in the All Chats filter tab.
*/
MobileAllChatsFilterAll,
/**
* User tapped the Favourites filter in the All Chats filter tab.
*/
MobileAllChatsFilterFavourites,
/**
* User tapped the People filter in the All Chats filter tab.
*/
MobileAllChatsFilterPeople,
/**
* User tapped the Unreads filter in the All Chats filter tab.
*/
MobileAllChatsFilterUnreads,
/**
* User disabled filters from the all chats layout settings.
*/
MobileAllChatsFiltersDisabled,
/**
* User enabled filters from the all chats layout settings.
*/
MobileAllChatsFiltersEnabled,
/**
* User disabled recents from the all chats layout settings.
*/
MobileAllChatsRecentsDisabled,
/**
* User enabled recents from the all chats layout settings.
*/
MobileAllChatsRecentsEnabled,
/**
* User tapped on Add to Home button on Room Details screen.
*/
MobileRoomAddHome,
/**
* User switched the favourite toggle on Room Details screen.
*/
MobileRoomFavouriteToggle,
/**
* User tapped on Leave Room button on Room Details screen.
*/
MobileRoomLeave,
/**
* User adjusted their favourite rooms using the context menu on a room
* in the room list.
*/
MobileRoomListRoomContextMenuFavouriteToggle,
/**
* User adjusted their unread rooms using the context menu on a room in
* the room list.
*/
MobileRoomListRoomContextMenuUnreadToggle,
/**
* User tapped on Threads button on Room screen.
*/
MobileRoomThreadListButton,
/**
* User tapped on a thread summary item on Room screen.
*/
MobileRoomThreadSummaryItem,
/**
* User validated the creation of a new space.
*/
MobileSpaceCreationValidated,
/**
* User tapped on the filter button on ThreadList screen.
*/
MobileThreadListFilterItem,
/**
* User selected a thread on ThreadList screen.
*/
MobileThreadListThreadItem,
/**
* User tapped the already selected space from the space list.
*/
SpacePanelSelectedSpace,
/**
* User tapped an unselected space from the space list -> space
* switching should occur.
*/
SpacePanelSwitchSpace,
/**
* User tapped an unselected sub space from the space list -> space
* switching should occur.
*/
SpacePanelSwitchSubSpace,
/**
* User clicked the create room button in the add existing room to space
* dialog in Element Web/Desktop.
*/
WebAddExistingToSpaceDialogCreateRoomButton,
/**
* User clicked the create DM button in the home page of Element
* Web/Desktop.
*/
WebHomeCreateChatButton,
/**
* User clicked the create room button in the home page of Element
* Web/Desktop.
*/
WebHomeCreateRoomButton,
/**
* User clicked the explore rooms button in the home page of Element
* Web/Desktop.
*/
WebHomeExploreRoomsButton,
/**
* User clicked on the mini avatar uploader in the home page of Element
* Web/Desktop.
*/
WebHomeMiniAvatarUploadButton,
/**
* User clicked the explore rooms button next to the search field at the
* top of the left panel in Element Web/Desktop.
*/
WebLeftPanelExploreRoomsButton,
/**
* User clicked on the avatar uploader in the profile settings of
* Element Web/Desktop.
*/
WebProfileSettingsAvatarUploadButton,
/**
* User interacted with pin to sidebar checkboxes in the quick settings
* menu of Element Web/Desktop.
*/
WebQuickSettingsPinToSidebarCheckbox,
/**
* User interacted with the theme dropdown in the quick settings menu of
* Element Web/Desktop.
*/
WebQuickSettingsThemeDropdown,
/**
* User accessed the room invite flow using the button at the top of the
* room member list in the right panel of Element Web/Desktop.
*/
WebRightPanelMemberListInviteButton,
/**
* User accessed room member list using the 'People' button in the right
* panel room summary card of Element Web/Desktop.
*/
WebRightPanelRoomInfoPeopleButton,
/**
* User accessed room settings using the 'Settings' button in the right
* panel room summary card of Element Web/Desktop.
*/
WebRightPanelRoomInfoSettingsButton,
/**
* User accessed room member list using the back button in the right
* panel user info card of Element Web/Desktop.
*/
WebRightPanelRoomUserInfoBackButton,
/**
* User invited someone to room by clicking invite on the right panel
* user info card in Element Web/Desktop.
*/
WebRightPanelRoomUserInfoInviteButton,
/**
* User clicked the threads 'show' filter dropdown in the threads panel
* in Element Web/Desktop.
*/
WebRightPanelThreadPanelFilterDropdown,
/**
* User clicked the create room button in the room directory of Element
* Web/Desktop.
*/
WebRoomDirectoryCreateRoomButton,
/**
* User clicked the Threads button in the top right of a room in Element
* Web/Desktop.
*/
WebRoomHeaderButtonsThreadsButton,
/**
* User adjusted their favourites using the context menu on the header
* of a room in Element Web/Desktop.
*/
WebRoomHeaderContextMenuFavouriteToggle,
/**
* User accessed the room invite flow using the context menu on the
* header of a room in Element Web/Desktop.
*/
WebRoomHeaderContextMenuInviteItem,
/**
* User interacted with leave action in the context menu on the header
* of a room in Element Web/Desktop.
*/
WebRoomHeaderContextMenuLeaveItem,
/**
* User accessed their room notification settings via the context menu
* on the header of a room in Element Web/Desktop.
*/
WebRoomHeaderContextMenuNotificationsItem,
/**
* User accessed room member list using the context menu on the header
* of a room in Element Web/Desktop.
*/
WebRoomHeaderContextMenuPeopleItem,
/**
* User accessed room settings using the context menu on the header of a
* room in Element Web/Desktop.
*/
WebRoomHeaderContextMenuSettingsItem,
/**
* User clicked the create DM button in the + context menu of the room
* list header in Element Web/Desktop.
*/
WebRoomListHeaderPlusMenuCreateChatItem,
/**
* User clicked the create room button in the + context menu of the room
* list header in Element Web/Desktop.
*/
WebRoomListHeaderPlusMenuCreateRoomItem,
/**
* User clicked the explore rooms button in the + context menu of the
* room list header in Element Web/Desktop.
*/
WebRoomListHeaderPlusMenuExploreRoomsItem,
/**
* User adjusted their favourites using the context menu on a room tile
* in the room list in Element Web/Desktop.
*/
WebRoomListRoomTileContextMenuFavouriteToggle,
/**
* User accessed the room invite flow using the context menu on a room
* tile in the room list in Element Web/Desktop.
*/
WebRoomListRoomTileContextMenuInviteItem,
/**
* User interacted with leave action in the context menu on a room tile
* in the room list in Element Web/Desktop.
*/
WebRoomListRoomTileContextMenuLeaveItem,
/**
* User marked a message as read using the context menu on a room tile
* in the room list in Element Web/Desktop.
*/
WebRoomListRoomTileContextMenuMarkRead,
/**
* User marked a room as unread using the context menu on a room tile in
* the room list in Element Web/Desktop.
*/
WebRoomListRoomTileContextMenuMarkUnread,
/**
* User accessed room settings using the context menu on a room tile in
* the room list in Element Web/Desktop.
*/
WebRoomListRoomTileContextMenuSettingsItem,
/**
* User accessed their room notification settings via the context menu
* on a room tile in the room list in Element Web/Desktop.
*/
WebRoomListRoomTileNotificationsMenu,
/**
* User clicked the create DM button in the + context menu of the rooms
* sublist in Element Web/Desktop.
*/
WebRoomListRoomsSublistPlusMenuCreateChatItem,
/**
* User clicked the create room button in the + context menu of the
* rooms sublist in Element Web/Desktop.
*/
WebRoomListRoomsSublistPlusMenuCreateRoomItem,
/**
* User clicked the explore rooms button in the + context menu of the
* rooms sublist in Element Web/Desktop.
*/
WebRoomListRoomsSublistPlusMenuExploreRoomsItem,
/**
* User clicked on the button to return to the user onboarding list in
* the room list in Element Web/Desktop.
*/
WebRoomListUserOnboardingButton,
/**
* User clicked on the button to close the user onboarding button in the
* room list in Element Web/Desktop.
*/
WebRoomListUserOnboardingIgnoreButton,
/**
* User interacted with leave action in the general tab of the room
* settings dialog in Element Web/Desktop.
*/
WebRoomSettingsLeaveButton,
/**
* User interacted with the prompt to create a new room when adjusting
* security settings in an existing room in Element Web/Desktop.
*/
WebRoomSettingsSecurityTabCreateNewRoomButton,
/**
* User clicked a thread summary in the timeline of a room in Element
* Web/Desktop.
*/
WebRoomTimelineThreadSummaryButton,
/**
* User interacted with the theme radio selector in the Appearance tab
* of Settings in Element Web/Desktop.
*/
WebSettingsAppearanceTabThemeSelector,
/**
* User interacted with the pre-built space checkboxes in the Sidebar
* tab of Settings in Element Web/Desktop.
*/
WebSettingsSidebarTabSpacesCheckbox,
/**
* User clicked the explore rooms button in the context menu of a space
* in Element Web/Desktop.
*/
WebSpaceContextMenuExploreRoomsItem,
/**
* User clicked the home button in the context menu of a space in
* Element Web/Desktop.
*/
WebSpaceContextMenuHomeItem,
/**
* User clicked the new room button in the context menu of a space in
* Element Web/Desktop.
*/
WebSpaceContextMenuNewRoomItem,
/**
* User clicked the new room button in the context menu on the space
* home in Element Web/Desktop.
*/
WebSpaceHomeCreateRoomButton,
/**
* User clicked the back button on a Thread view going back to the
* Threads Panel of Element Web/Desktop.
*/
WebThreadViewBackButton,
/**
* User clicked on the Threads Activity Centre button of Element
* Web/Desktop.
*/
WebThreadsActivityCentreButton,
/**
* User clicked on a room in the Threads Activity Centre of Element
* Web/Desktop.
*/
WebThreadsActivityCentreRoomItem,
/**
* User selected a thread in the Threads panel in Element Web/Desktop.
*/
WebThreadsPanelThreadItem,
/**
* User clicked the theme toggle button in the user menu of Element
* Web/Desktop.
*/
WebUserMenuThemeToggleButton,
/**
* User clicked on the send DM CTA in the header of the new user
* onboarding page in Element Web/Desktop.
*/
WebUserOnboardingHeaderSendDm,
/**
* User clicked on the action of the download apps task on the new user
* onboarding page in Element Web/Desktop.
*/
WebUserOnboardingTaskDownloadApps,
/**
* User clicked on the action of the enable notifications task on the
* new user onboarding page in Element Web/Desktop.
*/
WebUserOnboardingTaskEnableNotifications,
/**
* User clicked on the action of the find people task on the new user
* onboarding page in Element Web/Desktop.
*/
WebUserOnboardingTaskSendDm,
/**
* User clicked on the action of the your profile task on the new user
* onboarding page in Element Web/Desktop.
*/
WebUserOnboardingTaskSetupProfile,
}
enum class InteractionType {
Keyboard,
Pointer,
Touch,
}
override fun getName() = "Interaction"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
index?.let { put("index", it) }
interactionType?.let { put("interactionType", it.name) }
put("name", name.name)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,107 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user joins a room.
*/
data class JoinedRoom(
/**
* Whether the room is a DM.
*/
val isDM: Boolean,
/**
* Whether the room is a Space.
*/
val isSpace: Boolean,
/**
* The size of the room.
*/
val roomSize: RoomSize,
/**
* The trigger for a room being joined if known.
*/
val trigger: Trigger? = null,
) : VectorAnalyticsEvent {
enum class Trigger {
/**
* Room joined via an invite.
*/
Invite,
/**
* Room joined via link.
*/
MobilePermalink,
/**
* Room joined via a push/desktop notification.
*/
Notification,
/**
* Room joined via the public rooms directory.
*/
RoomDirectory,
/**
* Room joined via its preview.
*/
RoomPreview,
/**
* Room joined via the /join slash command.
*/
SlashCommand,
/**
* Room joined via the space hierarchy view.
*/
SpaceHierarchy,
/**
* Room joined via a timeline pill or link in another room.
*/
Timeline,
}
enum class RoomSize {
ElevenToOneHundred,
MoreThanAThousand,
One,
OneHundredAndOneToAThousand,
ThreeToTen,
Two,
}
override fun getName() = "JoinedRoom"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("isDM", isDM)
put("isSpace", isSpace)
put("roomSize", roomSize.name)
trigger?.let { put("trigger", it.name) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,353 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user changed screen on Element Android/iOS.
*/
data class MobileScreen(
/**
* How long the screen was displayed for in milliseconds.
*/
val durationMs: Int? = null,
val screenName: ScreenName,
) : VectorAnalyticsScreen {
enum class ScreenName {
/**
* The screen that displays the user's breadcrumbs.
*/
Breadcrumbs,
/**
* The screen shown to create a poll.
*/
CreatePollView,
/**
* The screen shown to create a new (non-direct) room.
*/
CreateRoom,
/**
* The screen shown to create a new space.
*/
CreateSpace,
/**
* The confirmation screen shown before deactivating an account.
*/
DeactivateAccount,
/**
* The tab on mobile that displays the dialpad.
*/
Dialpad,
/**
* The screen shown to edit a poll.
*/
EditPollView,
/**
* The Favourites tab on mobile that lists your favourite people/rooms.
*/
Favourites,
/**
* The form for the forgot password use case.
*/
ForgotPassword,
/**
* Legacy: The screen that shows information about a specific group.
*/
Group,
/**
* The Home tab on iOS | possibly the same on Android?
*/
Home,
/**
* The screen shown to share a link to download the app.
*/
InviteFriends,
/**
* Room accessed via space bottom sheet list.
*/
Invites,
/**
* The screen shown to share location.
*/
LocationSend,
/**
* The screen shown to view a shared location.
*/
LocationView,
/**
* The screen that displays the login flow (when the user already has an
* account).
*/
Login,
/**
* Legacy: The screen that shows all groups/communities you have joined.
*/
MyGroups,
/**
* The screen containing tests to help user to fix issues around
* notifications.
*/
NotificationTroubleshoot,
/**
* The People tab on mobile that lists all the DM rooms you have joined.
*/
People,
/**
* The screen that displays the registration flow (when the user wants
* to create an account).
*/
Register,
/**
* The screen that displays the messages and events received in a room.
*/
Room,
/**
* The room addresses screen shown from the Room Details screen.
*/
RoomAddresses,
/**
* The screen shown when tapping the name of a room from the Room
* screen.
*/
RoomDetails,
/**
* The screen that lists public rooms for you to discover.
*/
RoomDirectory,
/**
* The screen that lists all the user's rooms and let them filter the
* rooms.
*/
RoomFilter,
/**
* The screen that displays the list of members that are part of a room.
*/
RoomMembers,
/**
* The notifications settings screen shown from the Room Details screen.
*/
RoomNotifications,
/**
* The roles permissions screen shown from the Room Details screen.
*/
RoomPermissions,
/**
* Screen that displays room preview if user hasn't joined yet.
*/
RoomPreview,
/**
* The screen that allows you to search for messages/files in a specific
* room.
*/
RoomSearch,
/**
* The settings screen shown from the Room Details screen.
*/
RoomSettings,
/**
* The screen that allows you to see all of the files sent in a specific
* room.
*/
RoomUploads,
/**
* The Rooms tab on mobile that lists all the (non-direct) rooms you've
* joined.
*/
Rooms,
/**
* The Files tab shown in the global search screen on Mobile.
*/
SearchFiles,
/**
* The Messages tab shown in the global search screen on Mobile.
*/
SearchMessages,
/**
* The People tab shown in the global search screen on Mobile.
*/
SearchPeople,
/**
* The Rooms tab shown in the global search screen on Mobile.
*/
SearchRooms,
/**
* The global settings screen shown in the app.
*/
Settings,
/**
* The advanced settings screen (developer mode, rageshake, push
* notification rules).
*/
SettingsAdvanced,
/**
* The settings screen to change the default notification options.
*/
SettingsDefaultNotifications,
/**
* The settings screen with general profile settings.
*/
SettingsGeneral,
/**
* The Help and About screen.
*/
SettingsHelp,
/**
* The settings screen with list of the ignored users.
*/
SettingsIgnoredUsers,
/**
* The experimental features settings screen.
*/
SettingsLabs,
/**
* The settings screen with legals information.
*/
SettingsLegals,
/**
* The settings screen to manage notification mentions and keywords.
*/
SettingsMentionsAndKeywords,
/**
* The notifications settings screen.
*/
SettingsNotifications,
/**
* The preferences screen (theme, language, editor preferences, etc.
*/
SettingsPreferences,
/**
* The global security settings screen.
*/
SettingsSecurity,
/**
* The calls settings screen.
*/
SettingsVoiceVideo,
/**
* The sidebar shown on mobile with spaces, settings etc.
*/
Sidebar,
/**
* Room accessed via space bottom sheet list.
*/
SpaceBottomSheet,
/**
* Screen that displays the list of rooms and spaces of a space.
*/
SpaceExploreRooms,
/**
* Screen that displays the list of members of a space.
*/
SpaceMembers,
/**
* The bottom sheet that list all space options.
*/
SpaceMenu,
/**
* The screen shown to create a new direct room.
*/
StartChat,
/**
* The screen shown to select which room directory you'd like to use.
*/
SwitchDirectory,
/**
* Screen that displays list of threads for a room.
*/
ThreadList,
/**
* A screen that shows information about a room member.
*/
User,
/**
* The splash screen.
*/
Welcome,
}
override fun getName() = screenName.name
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
durationMs?.let { put("durationMs", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,41 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user runs the troubleshoot notification test suite.
*/
data class NotificationTroubleshoot(
/**
* Whether one or more tests are in error.
*/
val hasError: Boolean,
) : VectorAnalyticsEvent {
override fun getName() = "NotificationTroubleshoot"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("hasError", hasError)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,109 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered after timing an operation in the app.
*/
data class PerformanceTimer(
/**
* Client defined, can be used for debugging.
*/
val context: String? = null,
/**
* Client defined, an optional value to indicate how many items were
* handled during the operation.
*/
val itemCount: Int? = null,
/**
* The timer that is being reported.
*/
val name: Name,
/**
* The time reported by the timer in milliseconds.
*/
val timeMs: Int,
) : VectorAnalyticsEvent {
enum class Name {
/**
* The time spent parsing the response from an initial /sync request. In
* this case, `itemCount` should contain the number of joined rooms.
*/
InitialSyncParsing,
/**
* The time spent waiting for a response to an initial /sync request. In
* this case, `itemCount` should contain the number of joined rooms.
*/
InitialSyncRequest,
/**
* The time taken to display an event in the timeline that was opened
* from a notification.
*/
NotificationsOpenEvent,
/**
* The duration of a regular /sync request when resuming the app. In
* this case, `itemCount` should contain the number of joined rooms in
* the response.
*/
StartupIncrementalSync,
/**
* The duration of an initial /sync request during startup (if the store
* has been wiped). In this case, `itemCount` should contain the number
* of joined rooms.
*/
StartupInitialSync,
/**
* How long the app launch screen is displayed for.
*/
StartupLaunchScreen,
/**
* The time to preload data in the MXStore on iOS. In this case,
* `itemCount` should contain the number of rooms in the store.
*/
StartupStorePreload,
/**
* The time to load all data from the store (including
* StartupStorePreload time). In this case, `itemCount` should contain
* the number of rooms loaded into the session
*/
StartupStoreReady,
}
override fun getName() = "PerformanceTimer"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
context?.let { put("context", it) }
itemCount?.let { put("itemCount", it) }
put("name", name.name)
put("timeMs", timeMs)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,53 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user changes a permission status.
*/
data class PermissionChanged(
/**
* Whether the permission has been granted by the user.
*/
val granted: Boolean,
/**
* The name of the permission.
*/
val permission: Permission,
) : VectorAnalyticsEvent {
enum class Permission {
/**
* Permissions related to sending notifications have changed.
*/
Notification,
}
override fun getName() = "PermissionChanged"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("granted", granted)
put("permission", permission.name)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,63 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when a poll is created or edited.
*/
data class PollCreation(
/**
* Whether this poll has been created or edited.
*/
val action: Action,
/**
* Whether this poll is undisclosed.
*/
val isUndisclosed: Boolean,
/**
* Number of answers in the poll.
*/
val numberOfAnswers: Int,
) : VectorAnalyticsEvent {
enum class Action {
/**
* Newly created poll.
*/
Create,
/**
* Edit of an existing poll.
*/
Edit,
}
override fun getName() = "PollCreation"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("action", action.name)
put("isUndisclosed", isUndisclosed)
put("numberOfAnswers", numberOfAnswers)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when a poll has been ended.
*/
data class PollEnd(
/**
* Do not use this. Remove this property when the kotlin type generator
* can properly generate types without properties other than the event
* name.
*/
val doNotUse: Boolean? = null,
) : VectorAnalyticsEvent {
override fun getName() = "PollEnd"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
doNotUse?.let { put("doNotUse", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when a poll vote has been cast.
*/
data class PollVote(
/**
* Do not use this. Remove this property when the kotlin type generator
* can properly generate types without properties other than the event
* name.
*/
val doNotUse: Boolean? = null,
) : VectorAnalyticsEvent {
override fun getName() = "PollVote"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
doNotUse?.let { put("doNotUse", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,137 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when a moderation action is performed within a room.
*/
data class RoomModeration(
/**
* The action that was performed.
*/
val action: Action,
/**
* When the action sets a particular power level, this is the suggested
* role for that the power level.
*/
val role: Role? = null,
) : VectorAnalyticsEvent {
enum class Action {
/**
* Banned a room member.
*/
BanMember,
/**
* Changed a room member's power level.
*/
ChangeMemberRole,
/**
* Changed the power level required to ban room members.
*/
ChangePermissionsBanMembers,
/**
* Changed the power level required to invite users to the room.
*/
ChangePermissionsInviteUsers,
/**
* Changed the power level required to kick room members.
*/
ChangePermissionsKickMembers,
/**
* Changed the power level required to redact messages in the room.
*/
ChangePermissionsRedactMessages,
/**
* Changed the power level required to set the room's avatar.
*/
ChangePermissionsRoomAvatar,
/**
* Changed the power level required to set the room's name.
*/
ChangePermissionsRoomName,
/**
* Changed the power level required to set the room's topic.
*/
ChangePermissionsRoomTopic,
/**
* Changed the power level required to send messages in the room.
*/
ChangePermissionsSendMessages,
/**
* Kicked a room member.
*/
KickMember,
/**
* Reset all of the room permissions back to their default values.
*/
ResetPermissions,
/**
* Unbanned a room member.
*/
UnbanMember,
}
enum class Role {
/**
* A power level of 100.
*/
Administrator,
/**
* A power level of 50.
*/
Moderator,
/**
* Any other power level.
*/
Other,
/**
* A power level of 0.
*/
User,
}
override fun getName() = "RoomModeration"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("action", action.name)
role?.let { put("role", it.name) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,84 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered once onboarding has completed, but only if the user registered a
* new account.
*/
data class Signup(
/**
* The type of authentication that was used to sign up.
*/
val authenticationType: AuthenticationType,
) : VectorAnalyticsEvent {
enum class AuthenticationType {
/**
* Social login using Apple.
*/
Apple,
/**
* Social login using Facebook.
*/
Facebook,
/**
* Social login using GitHub.
*/
GitHub,
/**
* Social login using GitLab.
*/
GitLab,
/**
* Social login using Google.
*/
Google,
/**
* Registration using some other mechanism such as fallback.
*/
Other,
/**
* Registration with a username and password.
*/
Password,
/**
* Registration using another SSO provider.
*/
SSO,
}
override fun getName() = "Signup"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("authenticationType", authenticationType.name)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,46 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user runs a slash command in their composer.
*/
data class SlashCommand(
/**
* The name of this command.
*/
val command: Command,
) : VectorAnalyticsEvent {
enum class Command {
Invite,
Part,
}
override fun getName() = "SlashCommand"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("command", command.name)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,61 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Super Properties are properties associated with events that are sent with
* every capture call, be it a $pageview, an autocaptured button click, or
* anything else.
*/
data class SuperProperties(
/**
* Used by web to identify the platform (Web Platform/Electron Platform).
*/
val appPlatform: String? = null,
/**
* Which crypto backend is the client currently using.
*/
val cryptoSDK: CryptoSDK? = null,
/**
* Version of the crypto backend.
*/
val cryptoSDKVersion: String? = null,
) {
enum class CryptoSDK {
/**
* Legacy crypto backend specific to each platform.
*/
Legacy,
/**
* Cross-platform crypto backend written in Rust.
*/
Rust,
}
fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
appPlatform?.let { put("appPlatform", it) }
cryptoSDK?.let { put("cryptoSDK", it.name) }
cryptoSDKVersion?.let { put("cryptoSDKVersion", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,66 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user becomes unauthenticated without actually clicking
* sign out(E.g. Due to expiry of an access token without a way to refresh).
*/
data class UnauthenticatedError(
/**
* The error code as defined in matrix spec. The source of this error is
* from the homeserver.
*/
val errorCode: ErrorCode,
/**
* The reason for the error. The source of this error is from the
* homeserver, the reason can vary and is subject to change so there is
* no enum of possible values.
*/
val errorReason: String,
/**
* Whether the auth mechanism is refresh-token-based.
*/
val refreshTokenAuth: Boolean,
/**
* Whether a soft logout or hard logout was triggered.
*/
val softLogout: Boolean,
) : VectorAnalyticsEvent {
enum class ErrorCode {
M_FORBIDDEN,
M_UNKNOWN,
M_UNKNOWN_TOKEN,
}
override fun getName() = "UnauthenticatedError"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
put("errorCode", errorCode.name)
put("errorReason", errorReason)
put("refreshTokenAuth", refreshTokenAuth)
put("softLogout", softLogout)
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,98 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* The user properties to apply when identifying. This is not an event
* definition. These properties must all be device independent.
*/
data class UserProperties(
/**
* The active filter in the All Chats screen.
*/
val allChatsActiveFilter: AllChatsActiveFilter? = null,
/**
* The selected messaging use case during the onboarding flow.
*/
val ftueUseCaseSelection: FtueUseCaseSelection? = null,
/**
* Number of joined rooms the user has favourited.
*/
val numFavouriteRooms: Int? = null,
/**
* Number of spaces (and sub-spaces) the user is joined to.
*/
val numSpaces: Int? = null,
) {
enum class FtueUseCaseSelection {
/**
* The third option, Communities.
*/
CommunityMessaging,
/**
* The first option, Friends and family.
*/
PersonalMessaging,
/**
* The footer option to skip the question.
*/
Skip,
/**
* The second option, Teams.
*/
WorkMessaging,
}
enum class AllChatsActiveFilter {
/**
* Filters are activated and All is selected.
*/
All,
/**
* Filters are activated and Favourites is selected.
*/
Favourites,
/**
* Filters are activated and People is selected.
*/
People,
/**
* Filters are activated and Unreads is selected.
*/
Unreads,
}
fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) }
ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
numFavouriteRooms?.let { put("numFavouriteRooms", it) }
numSpaces?.let { put("numSpaces", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -1,312 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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
*
* http://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.
*/
package im.vector.app.features.analytics.plan
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
// https://github.com/matrix-org/matrix-analytics-events/
/**
* Triggered when the user changes rooms.
*/
data class ViewRoom(
/**
* active space when user navigated to the room.
*/
val activeSpace: ActiveSpace? = null,
/**
* Whether the room is a DM.
*/
val isDM: Boolean? = null,
/**
* Whether the room is a Space.
*/
val isSpace: Boolean? = null,
/**
* The reason for the room change if known.
*/
val trigger: Trigger? = null,
/**
* Whether the interaction was performed via the keyboard input.
*/
val viaKeyboard: Boolean? = null,
) : VectorAnalyticsEvent {
enum class Trigger {
/**
* Room accessed due to being just created.
*/
Created,
/**
* Room switched due to user interacting with a message search result.
*/
MessageSearch,
/**
* Room switched due to user selecting a user to go to a DM with.
*/
MessageUser,
/**
* Room accessed via space explore.
*/
MobileExploreRooms,
/**
* Room switched due to user interacting with a file search result.
*/
MobileFileSearch,
/**
* Room accessed via interacting with the incall screen.
*/
MobileInCall,
/**
* Room accessed during external sharing.
*/
MobileLinkShare,
/**
* Room accessed via link.
*/
MobilePermalink,
/**
* Room accessed via interacting with direct chat item in the room
* contact detail screen.
*/
MobileRoomMemberDetail,
/**
* Room accessed via preview.
*/
MobileRoomPreview,
/**
* Room switched due to user interacting with a room search result.
*/
MobileRoomSearch,
/**
* Room accessed via interacting with direct chat item in the search
* contact detail screen.
*/
MobileSearchContactDetail,
/**
* Room accessed via space bottom sheet list.
*/
MobileSpaceBottomSheet,
/**
* Room accessed via interacting with direct chat item in the space
* contact detail screen.
*/
MobileSpaceMemberDetail,
/**
* Room accessed via space members list.
*/
MobileSpaceMembers,
/**
* Space accessed via interacting with the space menu.
*/
MobileSpaceMenu,
/**
* Space accessed via interacting with a space settings menu item.
*/
MobileSpaceSettings,
/**
* Room accessed via a push/desktop notification.
*/
Notification,
/**
* Room accessed via the predecessor link at the top of the upgraded
* room.
*/
Predecessor,
/**
* Room accessed via the public rooms directory.
*/
RoomDirectory,
/**
* Room accessed via the room list.
*/
RoomList,
/**
* Room accessed via a shortcut.
*/
Shortcut,
/**
* Room accessed via a slash command in Element Web/Desktop like /goto.
*/
SlashCommand,
/**
* Room accessed via the space hierarchy view.
*/
SpaceHierarchy,
/**
* Room accessed via a timeline pill or link in another room.
*/
Timeline,
/**
* Room accessed via a tombstone at the bottom of a predecessor room.
*/
Tombstone,
/**
* Room switched due to user interacting with incoming verification
* request.
*/
VerificationRequest,
/**
* Room switched due to accepting a call in a different room in Element
* Web/Desktop.
*/
WebAcceptCall,
/**
* Room switched due to making a call via the dial pad in Element
* Web/Desktop.
*/
WebDialPad,
/**
* Room accessed via interacting with the floating call or Jitsi PIP in
* Element Web/Desktop.
*/
WebFloatingCallWindow,
/**
* Room accessed via the shortcut in Element Web/Desktop's forward
* modal.
*/
WebForwardShortcut,
/**
* Room accessed via the Element Web/Desktop horizontal breadcrumbs at
* the top of the room list.
*/
WebHorizontalBreadcrumbs,
/**
* Room accessed via an Element Web/Desktop keyboard shortcut like go to
* next room with unread messages.
*/
WebKeyboardShortcut,
/**
* Room accessed via Element Web/Desktop's notification panel.
*/
WebNotificationPanel,
/**
* Room accessed via the predecessor link in Settings > Advanced in
* Element Web/Desktop.
*/
WebPredecessorSettings,
/**
* Room accessed via clicking on a notifications badge on a room list
* sublist in Element Web/Desktop.
*/
WebRoomListNotificationBadge,
/**
* Room switched due to the user changing space in Element Web/Desktop.
*/
WebSpaceContextSwitch,
/**
* Room accessed via clicking on the notifications badge on the
* currently selected space in Element Web/Desktop.
*/
WebSpacePanelNotificationBadge,
/**
* Room accessed via interacting with the Threads Activity Centre in
* Element Web/Desktop.
*/
WebThreadsActivityCentre,
/**
* Room accessed via Element Web/Desktop's Unified Search modal.
*/
WebUnifiedSearch,
/**
* Room accessed via the Element Web/Desktop vertical breadcrumb hover
* menu.
*/
WebVerticalBreadcrumbs,
/**
* Room switched due to widget interaction.
*/
Widget,
}
enum class ActiveSpace {
/**
* Active space is Home.
*/
Home,
/**
* Active space is a meta space.
*/
Meta,
/**
* Active space is a private space.
*/
Private,
/**
* Active space is a public space.
*/
Public,
}
override fun getName() = "ViewRoom"
override fun getProperties(): Map<String, Any>? {
return mutableMapOf<String, Any>().apply {
activeSpace?.let { put("activeSpace", it.name) }
isDM?.let { put("isDM", it) }
isSpace?.let { put("isSpace", it) }
trigger?.let { put("trigger", it.name) }
viaKeyboard?.let { put("viaKeyboard", it) }
}.takeIf { it.isNotEmpty() }
}
}

View file

@ -52,7 +52,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class ResendMessage(val eventId: String) : RoomDetailAction() data class ResendMessage(val eventId: String) : RoomDetailAction()
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction() data class CancelSend(val event: TimelineEvent, val force: Boolean) : RoomDetailAction()
data class VoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction() data class VoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction()

View file

@ -66,6 +66,10 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
val mimeType: String? val mimeType: String?
) : RoomDetailViewEvents() ) : RoomDetailViewEvents()
data class RevokeFilePermission(
val uri: Uri
) : RoomDetailViewEvents()
data class DisplayAndAcceptCall(val call: WebRtcCall) : RoomDetailViewEvents() data class DisplayAndAcceptCall(val call: WebRtcCall) : RoomDetailViewEvents()
object DisplayPromptForIntegrationManager : RoomDetailViewEvents() object DisplayPromptForIntegrationManager : RoomDetailViewEvents()

View file

@ -450,8 +450,11 @@ class TimelineFragment :
is RoomDetailViewEvents.ScDbgReadTracking -> handleScDbgReadTracking(it) is RoomDetailViewEvents.ScDbgReadTracking -> handleScDbgReadTracking(it)
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget()
RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast() RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast()
// SC
RoomDetailViewEvents.JumpToBottom -> doJumpToBottom() RoomDetailViewEvents.JumpToBottom -> doJumpToBottom()
is RoomDetailViewEvents.SetInitialForceScroll -> setInitialForceScrollEnabled(it.enabled, stickToBottom = it.stickToBottom) is RoomDetailViewEvents.SetInitialForceScroll -> setInitialForceScrollEnabled(it.enabled, stickToBottom = it.stickToBottom)
// SC end
is RoomDetailViewEvents.RevokeFilePermission -> revokeFilePermission(it)
} }
} }
@ -1881,14 +1884,14 @@ class TimelineFragment :
private fun handleCancelSend(action: EventSharedAction.Cancel) { private fun handleCancelSend(action: EventSharedAction.Cancel) {
if (action.force) { if (action.force) {
timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, true)) timelineViewModel.handle(RoomDetailAction.CancelSend(action.event, true))
} else { } else {
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.dialog_title_confirmation) .setTitle(R.string.dialog_title_confirmation)
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message)) .setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
.setNegativeButton(R.string.no, null) .setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
timelineViewModel.handle(RoomDetailAction.CancelSend(action.eventId, false)) timelineViewModel.handle(RoomDetailAction.CancelSend(action.event, false))
} }
.show() .show()
} }
@ -2402,6 +2405,21 @@ class TimelineFragment :
} }
} }
private fun revokeFilePermission(revokeFilePermission: RoomDetailViewEvents.RevokeFilePermission) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requireContext().revokeUriPermission(
requireContext().applicationContext.packageName,
revokeFilePermission.uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} else {
requireContext().revokeUriPermission(
revokeFilePermission.uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
}
override fun onTapToReturnToCall() { override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call -> callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent( VectorCallActivity.newIntent(

View file

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail
import android.net.Uri import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.core.net.toUri
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
@ -97,6 +98,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
@ -124,6 +126,8 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
@ -868,7 +872,7 @@ class TimelineViewModel @AssistedInject constructor(
} }
fun getRoom(roomId: String): RoomSummary? = fun getRoom(roomId: String): RoomSummary? =
session.roomService().getRoomSummary(roomId) session.roomService().getRoomSummary(roomId)
private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) { private fun handleComposerFocusChange(action: RoomDetailAction.ComposerFocusChange) {
if (action.focused) { if (action.focused) {
@ -1197,18 +1201,17 @@ class TimelineViewModel @AssistedInject constructor(
private fun handleCancel(action: RoomDetailAction.CancelSend) { private fun handleCancel(action: RoomDetailAction.CancelSend) {
if (room == null) return if (room == null) return
if (action.force) { // State must be in one of the sending states
room.sendService().cancelSend(action.eventId) if (action.force || action.event.root.sendState.isSending()) {
return room.sendService().cancelSend(action.event.eventId)
}
val targetEventId = action.eventId val clearContent = action.event.root.getClearContent()
room.getTimelineEvent(targetEventId)?.let { val messageContent = clearContent?.toModel<MessageContent>() as? MessageWithAttachmentContent
// State must be in one of the sending states messageContent?.getFileUrl()?.takeIf { !it.isMxcUrl() }?.let {
if (!it.root.sendState.isSending()) { _viewEvents.post(RoomDetailViewEvents.RevokeFilePermission(it.toUri()))
Timber.e("Cannot cancel message, it is not sending")
return
} }
room.sendService().cancelSend(targetEventId) } else {
Timber.e("Cannot cancel message, it is not sending")
} }
} }
@ -1270,7 +1273,22 @@ class TimelineViewModel @AssistedInject constructor(
if (room == null) return if (room == null) return
viewModelScope.launch { viewModelScope.launch {
val event = try { val event = try {
room.reportingService().reportContent(action.eventId, -100, action.reason) if (action.user && action.senderId != null) {
// When reporting a user, use the user state event if available (it should always be available)
val userStateEventId = room.stateService()
.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(action.senderId))
?.eventId
// If not found fallback to the provided event
val eventId = userStateEventId ?: action.eventId
room.reportingService()
.reportContent(
eventId = eventId,
score = -100,
reason = action.reason
)
} else {
room.reportingService().reportContent(action.eventId, -100, action.reason)
}
RoomDetailViewEvents.ActionSuccess(action) RoomDetailViewEvents.ActionSuccess(action)
} catch (failure: Throwable) { } catch (failure: Throwable) {
RoomDetailViewEvents.ActionFailure(action, failure) RoomDetailViewEvents.ActionFailure(action, failure)

View file

@ -49,6 +49,7 @@ import dagger.hilt.android.AndroidEntryPoint
import de.spiritcroc.util.ThumbnailGenerationVideoDownloadDecider import de.spiritcroc.util.ThumbnailGenerationVideoDownloadDecider
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.error.fatalError import im.vector.app.core.error.fatalError
import im.vector.app.core.extensions.orEmpty
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
@ -278,7 +279,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
it.isRecordingVoiceBroadcast && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause) it.isRecordingVoiceBroadcast && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause)
else -> { else -> {
timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause) timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause)
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString())) messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.formattedText ?: composer.text.orEmpty().toString()))
} }
} }
} }
@ -460,7 +461,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
} }
override fun onTextChanged(text: CharSequence) { override fun onTextChanged(text: CharSequence) {
messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(text)) messageComposerViewModel.handle(MessageComposerAction.OnTextChanged(composer.formattedText ?: text))
} }
override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state -> override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state ->

View file

@ -23,6 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
sealed class EventSharedAction( sealed class EventSharedAction(
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
@ -71,7 +72,7 @@ sealed class EventSharedAction(
data class Redact(val eventId: String, val askForReason: Boolean, val dialogTitleRes: Int, val dialogDescriptionRes: Int) : data class Redact(val eventId: String, val askForReason: Boolean, val dialogTitleRes: Int, val dialogDescriptionRes: Int) :
EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true) EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true)
data class Cancel(val eventId: String, val force: Boolean) : data class Cancel(val event: TimelineEvent, val force: Boolean) :
EventSharedAction(R.string.action_cancel, R.drawable.ic_close_round) EventSharedAction(R.string.action_cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : data class ViewSource(val content: String) :

View file

@ -313,7 +313,7 @@ class MessageActionsViewModel @AssistedInject constructor(
private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) { private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) {
// TODO is uploading attachment? // TODO is uploading attachment?
if (canCancel(timelineEvent)) { if (canCancel(timelineEvent)) {
add(EventSharedAction.Cancel(timelineEvent.eventId, false)) add(EventSharedAction.Cancel(timelineEvent, false))
} }
} }
@ -321,7 +321,7 @@ class MessageActionsViewModel @AssistedInject constructor(
// If sent but not synced (synapse stuck at bottom bug) // If sent but not synced (synapse stuck at bottom bug)
// Still offer action to cancel (will only remove local echo) // Still offer action to cancel (will only remove local echo)
timelineEvent.root.eventId?.let { timelineEvent.root.eventId?.let {
add(EventSharedAction.Cancel(it, true)) add(EventSharedAction.Cancel(timelineEvent, true))
} }
// TODO Can be redacted // TODO Can be redacted

View file

@ -30,6 +30,10 @@ import im.vector.app.features.home.HomeActivity
import im.vector.lib.core.utils.compat.getParcelableCompat import im.vector.lib.core.utils.compat.getParcelableCompat
import timber.log.Timber import timber.log.Timber
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO define an end-of-life date for this implementation.
@AndroidEntryPoint @AndroidEntryPoint
class QrCodeLoginActivity : SimpleFragmentActivity() { class QrCodeLoginActivity : SimpleFragmentActivity() {

View file

@ -174,15 +174,20 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
} }
private fun handleReportAction() { private fun handleReportAction() {
room ?: return
viewModelScope.launch { viewModelScope.launch {
val event = try { val event = try {
// The API need an Event, use the latest Event. // The API needs an Event, use user state event if available (it should always be available)
val latestEventId = room?.roomSummary()?.latestPreviewableEvent?.eventId ?: return@launch val userStateEventId = room.stateService()
.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(initialState.userId))
?.eventId
// If not found fallback to the latest event
val eventId = (userStateEventId ?: room.roomSummary()?.latestPreviewableEvent?.eventId) ?: return@launch
room.reportingService() room.reportingService()
.reportContent( .reportContent(
eventId = latestEventId, eventId = eventId,
score = -100, score = -100,
reason = "Reporting user ${initialState.userId} (eventId is not relevant)" reason = "Reporting user ${initialState.userId}"
) )
RoomMemberProfileViewEvents.OnReportActionSuccess RoomMemberProfileViewEvents.OnReportActionSuccess
} catch (failure: Throwable) { } catch (failure: Throwable) {

File diff suppressed because one or more lines are too long

View file

@ -23,6 +23,7 @@ import im.vector.app.test.fakes.FakeAnalyticsTracker
import im.vector.app.test.fakes.FakeClock import im.vector.app.test.fakes.FakeClock
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.shared.createTimberTestRule import im.vector.app.test.shared.createTimberTestRule
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
@ -60,9 +61,24 @@ class DecryptionFailureTrackerTest {
private val fakeClock = FakeClock() private val fakeClock = FakeClock()
val reportedEvents = mutableSetOf<String>()
private val fakePersistence = mockk<ReportedDecryptionFailurePersistence> {
coEvery { load() } just runs
coEvery { persist() } just runs
coEvery { markAsReported(any()) } coAnswers {
reportedEvents.add(firstArg())
}
every { hasBeenReported(any()) } answers {
reportedEvents.contains(firstArg())
}
}
private val decryptionFailureTracker = DecryptionFailureTracker( private val decryptionFailureTracker = DecryptionFailureTracker(
fakeAnalyticsTracker, fakeAnalyticsTracker,
fakeActiveSessionDataSource.instance, fakeActiveSessionDataSource.instance,
fakePersistence,
fakeClock fakeClock
) )
@ -101,6 +117,7 @@ class DecryptionFailureTrackerTest {
@Before @Before
fun setupTest() { fun setupTest() {
reportedEvents.clear()
fakeMxOrgTestSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false) fakeMxOrgTestSession.fakeCryptoService.fakeCrossSigningService.givenIsCrossSigningVerifiedReturns(false)
} }

View file

@ -16,9 +16,6 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import com.posthog.android.Properties
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.test.fakes.FakeAnalyticsStore import im.vector.app.test.fakes.FakeAnalyticsStore
import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory import im.vector.app.test.fakes.FakeLateInitUserPropertiesFactory
import im.vector.app.test.fakes.FakePostHog import im.vector.app.test.fakes.FakePostHog
@ -128,7 +125,7 @@ class DefaultVectorAnalyticsTest {
defaultVectorAnalytics.screen(A_SCREEN_EVENT) defaultVectorAnalytics.screen(A_SCREEN_EVENT)
fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), A_SCREEN_EVENT.toPostHogProperties()) fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), A_SCREEN_EVENT.getProperties())
} }
@Test @Test
@ -146,7 +143,7 @@ class DefaultVectorAnalyticsTest {
defaultVectorAnalytics.capture(AN_EVENT) defaultVectorAnalytics.capture(AN_EVENT)
fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.toPostHogProperties()) fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.getProperties().clearNulls())
} }
@Test @Test
@ -176,16 +173,16 @@ class DefaultVectorAnalyticsTest {
fakeSentryAnalytics.verifyNoErrorTracking() fakeSentryAnalytics.verifyNoErrorTracking()
} }
}
private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? { private fun Map<String, Any?>?.clearNulls(): Map<String, Any>? {
return getProperties()?.let { properties -> if (this == null) return null
Properties().also { it.putAll(properties) }
} val nonNulls = HashMap<String, Any>()
} this.forEach { (key, value) ->
if (value != null) {
private fun VectorAnalyticsEvent.toPostHogProperties(): Properties? { nonNulls[key] = value
return getProperties()?.let { properties -> }
Properties().also { it.putAll(properties) } }
return nonNulls
} }
} }

View file

@ -17,8 +17,7 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import android.os.Looper import android.os.Looper
import com.posthog.android.PostHog import com.posthog.PostHogInterface
import com.posthog.android.Properties
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -36,16 +35,19 @@ class FakePostHog {
every { Looper.getMainLooper() } returns looper every { Looper.getMainLooper() } returns looper
} }
val instance = mockk<PostHog>(relaxed = true) val instance = mockk<PostHogInterface>(relaxed = true)
fun verifyOptOutStatus(optedOut: Boolean) { fun verifyOptOutStatus(optedOut: Boolean) {
verify { instance.optOut(optedOut) } if (optedOut) {
verify { instance.optOut() }
} else {
verify { instance.optIn() }
}
} }
fun verifyIdentifies(analyticsId: String, userProperties: UserProperties?) { fun verifyIdentifies(analyticsId: String, userProperties: UserProperties?) {
verify { verify {
val postHogProperties = userProperties?.getProperties() val postHogProperties = userProperties?.getProperties()
?.let { rawProperties -> Properties().also { it.putAll(rawProperties) } }
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
instance.identify(analyticsId, postHogProperties, null) instance.identify(analyticsId, postHogProperties, null)
} }
@ -55,7 +57,7 @@ class FakePostHog {
verify { instance.reset() } verify { instance.reset() }
} }
fun verifyScreenTracked(name: String, properties: Properties?) { fun verifyScreenTracked(name: String, properties: Map<String, Any>?) {
verify { instance.screen(name, properties) } verify { instance.screen(name, properties) }
} }
@ -63,12 +65,11 @@ class FakePostHog {
verify(exactly = 0) { verify(exactly = 0) {
instance.screen(any()) instance.screen(any())
instance.screen(any(), any()) instance.screen(any(), any())
instance.screen(any(), any(), any())
} }
} }
fun verifyEventTracked(name: String, properties: Properties?) { fun verifyEventTracked(name: String, properties: Map<String, Any>?) {
verify { instance.capture(name, properties) } verify { instance.capture(name, null, properties) }
} }
fun verifyNoEventTracking() { fun verifyNoEventTracking() {

View file

@ -16,12 +16,12 @@
package im.vector.app.test.fakes package im.vector.app.test.fakes
import com.posthog.android.PostHog import com.posthog.PostHogInterface
import im.vector.app.features.analytics.impl.PostHogFactory import im.vector.app.features.analytics.impl.PostHogFactory
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
class FakePostHogFactory(postHog: PostHog) { class FakePostHogFactory(postHog: PostHogInterface) {
val instance = mockk<PostHogFactory>().also { val instance = mockk<PostHogFactory>().also {
every { it.createPosthog() } returns postHog every { it.createPosthog() } returns postHog
} }