diff --git a/.github/workflows/sanity_test.yml b/.github/workflows/sanity_test.yml
index 93e4686fe7..483926fa1f 100644
--- a/.github/workflows/sanity_test.yml
+++ b/.github/workflows/sanity_test.yml
@@ -69,9 +69,10 @@ jobs:
touch emulator.log
chmod 777 emulator.log
adb logcat >> emulator.log &
- ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots
+ ./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 )
- name: Upload Test Report Log
uses: actions/upload-artifact@v2
+ if: always()
with:
name: sanity-error-results
path: |
diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index f99842f067..ed572b573f 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -28,6 +28,7 @@
previewable
previewables
pstn
+ rageshake
riotx
signin
signout
diff --git a/.idea/icon.png b/.idea/icon.png
new file mode 100644
index 0000000000..6f7872211b
Binary files /dev/null and b/.idea/icon.png differ
diff --git a/changelog.d/2782.misc b/changelog.d/2782.misc
new file mode 100644
index 0000000000..dc20050369
--- /dev/null
+++ b/changelog.d/2782.misc
@@ -0,0 +1 @@
+Collapse successive ACLs events in room timeline
diff --git a/changelog.d/3771.feature b/changelog.d/3771.feature
new file mode 100644
index 0000000000..c480bb649d
--- /dev/null
+++ b/changelog.d/3771.feature
@@ -0,0 +1 @@
+Open the room when user accepts an invite from the room list
\ No newline at end of file
diff --git a/changelog.d/4643.misc b/changelog.d/4643.misc
new file mode 100644
index 0000000000..3d86baa1a2
--- /dev/null
+++ b/changelog.d/4643.misc
@@ -0,0 +1 @@
+Home screen: Replacing search icon by filter icon in the top right menu
diff --git a/changelog.d/5104.misc b/changelog.d/5104.misc
new file mode 100644
index 0000000000..673614955b
--- /dev/null
+++ b/changelog.d/5104.misc
@@ -0,0 +1 @@
+Make Space creation screens more consistent
diff --git a/changelog.d/5123.feature b/changelog.d/5123.feature
new file mode 100644
index 0000000000..cb1a7adf08
--- /dev/null
+++ b/changelog.d/5123.feature
@@ -0,0 +1 @@
+Add completion for @room to notify everyone in a room
diff --git a/changelog.d/5136.misc b/changelog.d/5136.misc
new file mode 100644
index 0000000000..43404acc31
--- /dev/null
+++ b/changelog.d/5136.misc
@@ -0,0 +1 @@
+Defensive coding to ensure encryption when room was once e2e
\ No newline at end of file
diff --git a/changelog.d/5185.sdk b/changelog.d/5185.sdk
new file mode 100644
index 0000000000..9eda2e7c9b
--- /dev/null
+++ b/changelog.d/5185.sdk
@@ -0,0 +1 @@
+Deprecates Matrix.initialize and Matrix.getInstance in favour of the client providing its own singleton instance via Matrix.createInstance
\ No newline at end of file
diff --git a/changelog.d/5201.bugfix b/changelog.d/5201.bugfix
new file mode 100644
index 0000000000..f77ddcce84
--- /dev/null
+++ b/changelog.d/5201.bugfix
@@ -0,0 +1 @@
+Fix for call transfer with consult failing to make outgoing consultation call.
\ No newline at end of file
diff --git a/changelog.d/5225.misc b/changelog.d/5225.misc
new file mode 100644
index 0000000000..799a3a4d81
--- /dev/null
+++ b/changelog.d/5225.misc
@@ -0,0 +1 @@
+Replacing color "vctr_unread_room_badge" by "vctr_content_secondary"
diff --git a/changelog.d/5234.bugfix b/changelog.d/5234.bugfix
new file mode 100644
index 0000000000..2b5d4dee37
--- /dev/null
+++ b/changelog.d/5234.bugfix
@@ -0,0 +1 @@
+Analytics: Fixes missing use case identity values from within the onboarding flow
\ No newline at end of file
diff --git a/changelog.d/5243.bugfix b/changelog.d/5243.bugfix
new file mode 100644
index 0000000000..eb323c1ca4
--- /dev/null
+++ b/changelog.d/5243.bugfix
@@ -0,0 +1 @@
+Increments database schema to take advantage of homeserver capabilities entity migration (fixes crash in pre-release builds)
\ No newline at end of file
diff --git a/changelog.d/5254.misc b/changelog.d/5254.misc
new file mode 100644
index 0000000000..2ae642e9b7
--- /dev/null
+++ b/changelog.d/5254.misc
@@ -0,0 +1 @@
+Change preferred jitsi domain from `jitsi.riot.im` to `meet.element.io`
\ No newline at end of file
diff --git a/changelog.d/5290.feature b/changelog.d/5290.feature
new file mode 100644
index 0000000000..6f7e9aea7f
--- /dev/null
+++ b/changelog.d/5290.feature
@@ -0,0 +1 @@
+Support creating disclosed polls
\ No newline at end of file
diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml
index 48ac48a8ca..770b001893 100644
--- a/library/ui-styles/src/main/res/values/colors.xml
+++ b/library/ui-styles/src/main/res/values/colors.xml
@@ -57,11 +57,6 @@
-
- @color/palette_gray_200
- @color/palette_gray_250
- @color/palette_gray_250
-
@android:color/white
#FF181B21
diff --git a/library/ui-styles/src/main/res/values/text_appearances.xml b/library/ui-styles/src/main/res/values/text_appearances.xml
index 4ad3fd493e..8e30dd00d6 100644
--- a/library/ui-styles/src/main/res/values/text_appearances.xml
+++ b/library/ui-styles/src/main/res/values/text_appearances.xml
@@ -59,6 +59,10 @@
- sans-serif-medium
+
+
-
\ No newline at end of file
+
diff --git a/library/ui-styles/src/main/res/values/theme_black.xml b/library/ui-styles/src/main/res/values/theme_black.xml
index c472a4fae5..44d4206d43 100644
--- a/library/ui-styles/src/main/res/values/theme_black.xml
+++ b/library/ui-styles/src/main/res/values/theme_black.xml
@@ -7,7 +7,6 @@
- - @color/vctr_unread_room_badge_black
- @color/vctr_fab_label_bg_black
- @color/vctr_fab_label_stroke_black
- @color/vctr_fab_label_color_black
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index b1d95c5439..100a07f41d 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -16,7 +16,6 @@
- @color/element_system_dark
- - @color/vctr_unread_room_badge_dark
- @color/vctr_fab_label_bg_dark
- @color/vctr_fab_label_stroke_dark
- @color/vctr_fab_label_color_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index dba39c97ca..39e78ee5b1 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -16,7 +16,6 @@
- @color/element_system_light
- - @color/vctr_unread_room_badge_light
- @color/vctr_fab_label_bg_light
- @color/vctr_fab_label_stroke_light
- @color/vctr_fab_label_color_light
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
index 901ba75d16..5fedff53f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
@@ -99,12 +99,31 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
private lateinit var instance: Matrix
private val isInit = AtomicBoolean(false)
+ /**
+ * Creates a new instance of Matrix, it's recommended to manage this instance as a singleton.
+ * To make use of the built in singleton use Matrix.initialize() and/or Matrix.getInstance(context) instead
+ **/
+ fun createInstance(context: Context, matrixConfiguration: MatrixConfiguration): Matrix {
+ return Matrix(context.applicationContext, matrixConfiguration)
+ }
+
+ /**
+ * Initializes a singleton instance of Matrix for the given MatrixConfiguration
+ * This instance will be returned by Matrix.getInstance(context)
+ */
+ @Deprecated("Use Matrix.createInstance and manage the instance manually")
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
if (isInit.compareAndSet(false, true)) {
instance = Matrix(context.applicationContext, matrixConfiguration)
}
}
+ /**
+ * Either provides an already initialized singleton Matrix instance or queries the application context for a MatrixConfiguration.Provider
+ * to lazily create and store the instance.
+ */
+ @Suppress("deprecation") // suppressing warning as this method is unused but is still provided for SDK clients
+ @Deprecated("Use Matrix.createInstance and manage the instance manually")
fun getInstance(context: Context): Matrix {
if (isInit.compareAndSet(false, true)) {
val appContext = context.applicationContext
@@ -113,7 +132,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
instance = Matrix(appContext, matrixConfiguration)
} else {
throw IllegalStateException("Matrix is not initialized properly." +
- " You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
+ " If you want to manage your own Matrix instance use Matrix.createInstance" +
+ " otherwise you should call Matrix.initialize or let your application implement MatrixConfiguration.Provider.")
}
}
return instance
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
index 306ed45500..c87f21d7ac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
@@ -66,6 +66,7 @@ data class MatrixConfiguration(
/**
* Can be implemented by your Application class.
*/
+ @Deprecated("Use Matrix.createInstance and manage the instance manually instead of Matrix.getInstance")
interface Provider {
fun providesMatrixConfiguration(): MatrixConfiguration
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
index 88268f0f86..76885d8545 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
@@ -50,6 +50,9 @@ interface PushRuleService {
// fun fulfilledBingRule(event: Event, rules: List): PushRule?
+ fun resolveSenderNotificationPermissionCondition(event: Event,
+ condition: SenderNotificationPermissionCondition): Boolean
+
interface PushRuleListener {
fun onEvents(pushEvents: PushEvents)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 17d7d96a38..650b8cc26d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -36,7 +36,19 @@ sealed class MatrixItem(
data class UserItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
+ MatrixItem(id, displayName?.removeSuffix(IRC_PATTERN), avatarUrl) {
+ init {
+ if (BuildConfig.DEBUG) checkId()
+ }
+
+ override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
+ }
+
+ data class EveryoneInRoomItem(override val id: String,
+ override val displayName: String = NOTIFY_EVERYONE,
+ override val avatarUrl: String? = null,
+ val roomDisplayName: String? = null) :
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -47,7 +59,7 @@ sealed class MatrixItem(
data class EventItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -58,7 +70,7 @@ sealed class MatrixItem(
data class RoomItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -69,7 +81,7 @@ sealed class MatrixItem(
data class SpaceItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -80,7 +92,7 @@ sealed class MatrixItem(
data class RoomAliasItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -91,7 +103,7 @@ sealed class MatrixItem(
data class GroupItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
- MatrixItem(id, displayName, avatarUrl) {
+ MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@@ -110,16 +122,22 @@ sealed class MatrixItem(
/**
* Return the prefix as defined in the matrix spec (and not extracted from the id)
*/
- fun getIdPrefix() = when (this) {
- is UserItem -> '@'
- is EventItem -> '$'
+ private fun getIdPrefix() = when (this) {
+ is UserItem -> '@'
+ is EventItem -> '$'
is SpaceItem,
- is RoomItem -> '!'
- is RoomAliasItem -> '#'
- is GroupItem -> '+'
+ is RoomItem,
+ is EveryoneInRoomItem -> '!'
+ is RoomAliasItem -> '#'
+ is GroupItem -> '+'
}
fun firstLetterOfDisplayName(): String {
+ val displayName = when (this) {
+ // use the room display name for the notify everyone item
+ is EveryoneInRoomItem -> roomDisplayName
+ else -> displayName
+ }
return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn ->
var startIndex = 0
@@ -152,7 +170,8 @@ sealed class MatrixItem(
}
companion object {
- private const val ircPattern = " (IRC)"
+ private const val IRC_PATTERN = " (IRC)"
+ const val NOTIFY_EVERYONE = "@room"
}
}
@@ -172,6 +191,8 @@ fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
+fun RoomSummary.toEveryoneInRoomMatrixItem() = MatrixItem.EveryoneInRoomItem(id = roomId, avatarUrl = avatarUrl, roomDisplayName = displayName)
+
// If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
index 82eced4371..2a58d731e5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -35,6 +35,8 @@ internal class CryptoSessionInfoProvider @Inject constructor(
) {
fun isRoomEncrypted(roomId: String): Boolean {
+ // We look at the presence at any m.room.encryption state event no matter if it's
+ // the latest one or if it is well formed
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.isEmpty(EventEntityFields.STATE_KEY)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 82fb565377..96ea5c03fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -240,6 +240,14 @@ internal interface IMXCryptoStore {
*/
fun getRoomAlgorithm(roomId: String): String?
+ /**
+ * This is a bit different than isRoomEncrypted
+ * A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not)
+ * But the crypto layer has additional guaranty to ensure that encryption would never been reverted
+ * It's defensive coding out of precaution (if ever state is reset)
+ */
+ fun roomWasOnceEncrypted(roomId: String): Boolean
+
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index 33578ba06a..a07827c033 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -631,7 +631,15 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
doRealmTransaction(realmConfiguration) {
- CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
+ CryptoRoomEntity.getOrCreate(it, roomId).let { entity ->
+ entity.algorithm = algorithm
+ // store anyway the new algorithm, but mark the room
+ // as having been encrypted once whatever, this can never
+ // go back to false
+ if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
+ entity.wasEncryptedOnce = true
+ }
+ }
}
}
@@ -641,6 +649,12 @@ internal class RealmCryptoStore @Inject constructor(
}
}
+ override fun roomWasOnceEncrypted(roomId: String): Boolean {
+ return doWithRealm(realmConfiguration) {
+ CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false
+ }
+ }
+
override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
index 685b2d2967..cac6499486 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo012
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
import timber.log.Timber
import javax.inject.Inject
@@ -46,7 +47,7 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration
// 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
- val schemaVersion = 14L
+ val schemaVersion = 15L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
@@ -65,5 +66,6 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration
if (oldVersion < 12) MigrateCryptoTo012(realm).perform()
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
+ if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt
new file mode 100644
index 0000000000..465c18555a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.crypto.store.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+// Version 15L adds wasEncryptedOnce field to CryptoRoomEntity
+class MigrateCryptoTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
+
+ override fun doMigrate(realm: DynamicRealm) {
+ realm.schema.get("CryptoRoomEntity")
+ ?.addField(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, Boolean::class.java)
+ ?.setNullable(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, true)
+ ?.transform {
+ val currentAlgorithm = it.getString(CryptoRoomEntityFields.ALGORITHM)
+ it.set(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, currentAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM)
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
index 711b698464..6167314b5a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
@@ -27,7 +27,10 @@ internal open class CryptoRoomEntity(
// Store the current outbound session for this room,
// to avoid re-create and re-share at each startup (if rotation not needed..)
// This is specific to megolm but not sure how to model it better
- var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null
+ var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null,
+ // a security to ensure that a room will never revert to not encrypted
+ // even if a new state event with empty encryption, or state is reset somehow
+ var wasEncryptedOnce: Boolean? = false
) :
RealmObject() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
index 3e821b8956..cdc7350f8b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
@@ -19,11 +19,13 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.pushrules.Action
+import org.matrix.android.sdk.api.pushrules.ConditionResolver
import org.matrix.android.sdk.api.pushrules.PushEvents
import org.matrix.android.sdk.api.pushrules.PushRuleService
import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.RuleScope
import org.matrix.android.sdk.api.pushrules.RuleSetKey
+import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
import org.matrix.android.sdk.api.pushrules.getActions
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
@@ -53,6 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
private val removePushRuleTask: RemovePushRuleTask,
private val pushRuleFinder: PushRuleFinder,
private val taskExecutor: TaskExecutor,
+ private val conditionResolver: ConditionResolver,
@SessionDatabase private val monarchy: Monarchy
) : PushRuleService {
@@ -143,6 +146,10 @@ internal class DefaultPushRuleService @Inject constructor(
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
}
+ override fun resolveSenderNotificationPermissionCondition(event: Event, condition: SenderNotificationPermissionCondition): Boolean {
+ return conditionResolver.resolveSenderNotificationPermissionCondition(event, condition)
+ }
+
override fun getKeywords(): LiveData> {
// Keywords are all content rules that don't start with '.'
val liveData = monarchy.findAllMappedWithChanges(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
index f21ee4346c..d5d2059969 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
@@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
-import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
@@ -47,7 +46,6 @@ internal class DefaultRelationService @AssistedInject constructor(
private val eventEditor: EventEditor,
private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory,
- private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper,
@@ -144,7 +142,7 @@ internal class DefaultRelationService @AssistedInject constructor(
?.also { saveLocalEcho(it) }
?: return null
- return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+ return eventSenderProcessor.postEvent(event)
}
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
@@ -200,7 +198,7 @@ internal class DefaultRelationService @AssistedInject constructor(
saveLocalEcho(it)
}
}
- return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+ return eventSenderProcessor.postEvent(event)
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt
index 4551f390e7..b54cd71e50 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt
@@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable
-import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
@@ -33,7 +32,6 @@ import javax.inject.Inject
internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory,
- private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val localEchoRepository: LocalEchoRepository) {
fun editTextMessage(targetEvent: TimelineEvent,
@@ -51,7 +49,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
} else if (targetEvent.root.sendState.isSent()) {
val event = eventFactory
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
- return sendReplaceEvent(roomId, event)
+ return sendReplaceEvent(event)
} else {
// Should we throw?
Timber.w("Can't edit a sending event")
@@ -72,7 +70,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
} else if (targetEvent.root.sendState.isSent()) {
val event = eventFactory
.createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options)
- return sendReplaceEvent(roomId, event)
+ return sendReplaceEvent(event)
} else {
Timber.w("Can't edit a sending event")
return NoOpCancellable
@@ -82,12 +80,12 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable {
val roomId = targetEvent.roomId
updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent)
- return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+ return eventSenderProcessor.postEvent(editedEvent)
}
- private fun sendReplaceEvent(roomId: String, editedEvent: Event): Cancelable {
+ private fun sendReplaceEvent(editedEvent: Event): Cancelable {
localEchoRepository.createLocalEcho(editedEvent)
- return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+ return eventSenderProcessor.postEvent(editedEvent)
}
fun editReply(replyToEdit: TimelineEvent,
@@ -107,7 +105,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
eventId = replyToEdit.eventId
) ?: return NoOpCancellable
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
- return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+ return eventSenderProcessor.postEvent(editedEvent)
} else if (replyToEdit.root.sendState.isSent()) {
val event = eventFactory.createReplaceTextOfReply(
roomId,
@@ -119,7 +117,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
compatibilityBodyText
)
.also { localEchoRepository.createLocalEcho(it) }
- return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
+ return eventSenderProcessor.postEvent(event)
} else {
// Should we throw?
Timber.w("Can't edit a sending event")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 8c0ea0ec4c..28c17f38b6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -46,7 +46,7 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable
-import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@@ -66,7 +66,7 @@ internal class DefaultSendService @AssistedInject constructor(
private val workManagerProvider: WorkManagerProvider,
@SessionId private val sessionId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
- private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
+ private val cryptoStore: IMXCryptoStore,
private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository,
private val eventSenderProcessor: EventSenderProcessor,
@@ -303,7 +303,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun internalSendMedia(allLocalEchoes: List, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
val cancelableBag = CancelableBag()
- allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
+ allLocalEchoes.groupBy { cryptoStore.roomWasOnceEncrypted(it.roomId!!) }
.apply {
keys.forEach { isRoomEncrypted ->
// Should never be empty
@@ -334,7 +334,7 @@ internal class DefaultSendService @AssistedInject constructor(
}
private fun sendEvent(event: Event): Cancelable {
- return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
+ return eventSenderProcessor.postEvent(event)
}
private fun createLocalEcho(event: Event) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index 33cb0db243..ccbfbfcded 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills
import android.text.SpannableString
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
+import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
import java.util.Collections
import javax.inject.Inject
@@ -51,6 +52,8 @@ internal class TextPillsUtils @Inject constructor(
val pills = spannableString
?.getSpans(0, text.length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
+ // we use the raw text for @room notification instead of a link
+ ?.filterNot { it.span.matrixItem is MatrixItem.EveryoneInRoomItem }
?.toMutableList()
?.takeIf { it.isNotEmpty() }
?: return null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
index eb69161614..5b4efa5df6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
@@ -26,9 +26,9 @@ import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.getRetryDelay
import org.matrix.android.sdk.api.failure.isLimitExceededError
import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.CoroutineSequencer
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
@@ -54,7 +54,7 @@ private const val MAX_RETRY_COUNT = 3
*/
@SessionScope
internal class EventSenderProcessorCoroutine @Inject constructor(
- private val cryptoService: CryptoService,
+ private val cryptoStore: IMXCryptoStore,
private val sessionParams: SessionParams,
private val queuedTaskFactory: QueuedTaskFactory,
private val taskExecutor: TaskExecutor,
@@ -92,7 +92,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
}
override fun postEvent(event: Event): Cancelable {
- return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
+ val shouldEncrypt = event.roomId?.let { cryptoStore.roomWasOnceEncrypted(it) } ?: false
+ return postEvent(event, shouldEncrypt)
}
override fun postEvent(event: Event, encrypt: Boolean): Cancelable {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index a7887d77f8..1c1d59fb3d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -119,9 +119,8 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.roomType = roomType
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
- // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
- Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
+ Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
index 0ecf91f6fa..97ae9b3a68 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
@@ -192,12 +192,14 @@ abstract class SyncService : Service() {
}
}
+ abstract fun provideMatrix(): Matrix
+
private fun initialize(intent: Intent?): Boolean {
if (intent == null) {
Timber.d("## Sync: initialize intent is null")
return false
}
- val matrix = Matrix.getInstance(applicationContext)
+ val matrix = provideMatrix()
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds())
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds())
diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt
index fb7b9dcb41..69fe63fb7b 100644
--- a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt
@@ -38,7 +38,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import androidx.test.platform.app.InstrumentationRegistry
+import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.features.MainActivity
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.home.HomeActivity
@@ -47,7 +47,6 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.session.Session
@RunWith(AndroidJUnit4::class)
@@ -61,8 +60,7 @@ class SecurityBootstrapTest : VerificationTestBase() {
@Before
fun createSessionWithCrossSigning() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val matrix = Matrix.getInstance(context)
+ val matrix = getMatrixInstance()
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
stubAllExternalIntents()
diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt
index 982a421425..c82b543a08 100644
--- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt
@@ -33,7 +33,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import androidx.test.platform.app.InstrumentationRegistry
+import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.features.MainActivity
import im.vector.app.features.home.HomeActivity
import org.hamcrest.CoreMatchers.not
@@ -41,7 +41,6 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
@@ -66,8 +65,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
@Before
fun createSessionWithCrossSigning() {
- val context = InstrumentationRegistry.getInstrumentation().targetContext
- val matrix = Matrix.getInstance(context)
+ val matrix = getMatrixInstance()
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync {
diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt
index c51ff29669..80d8315a0e 100644
--- a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt
@@ -34,6 +34,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.features.MainActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
@@ -45,7 +46,6 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
@@ -67,7 +67,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
@Before
fun createSessionWithCrossSigningAnd4S() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
- val matrix = Matrix.getInstance(context)
+ val matrix = getMatrixInstance()
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync {
@@ -90,7 +90,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
runBlocking {
task.execute(Params(
- userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
+ userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) {
promise.resume(
UserPasswordAuth(
diff --git a/vector/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt b/vector/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt
new file mode 100644
index 0000000000..322f5fa23d
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/core/utils/TestMatrixHelper.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 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.core.utils
+
+import androidx.test.platform.app.InstrumentationRegistry
+import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
+import org.matrix.android.sdk.api.Matrix
+import org.matrix.android.sdk.api.MatrixConfiguration
+
+fun getMatrixInstance(): Matrix {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val configuration = MatrixConfiguration(
+ roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context)
+ )
+ return Matrix.createInstance(context, configuration)
+}
diff --git a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt
index 2e329ebb6b..2939dcf4e0 100644
--- a/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt
+++ b/vector/src/androidTest/java/im/vector/app/espresso/tools/ScreenshotFailureRule.kt
@@ -40,7 +40,7 @@ private val deviceLanguage = Locale.getDefault().language
class ScreenshotFailureRule : TestWatcher() {
override fun failed(e: Throwable?, description: Description) {
- val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())}"
+ val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HHmmss").format(Date())}"
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
storeFailureScreenshot(bitmap, screenShotName)
}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
index d625cf0390..417d28d625 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt
@@ -16,10 +16,12 @@
package im.vector.app.ui
+import android.Manifest
import androidx.test.espresso.IdlingPolicies
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.test.rule.GrantPermissionRule
import im.vector.app.R
import im.vector.app.espresso.tools.ScreenshotFailureRule
import im.vector.app.features.MainActivity
@@ -43,6 +45,7 @@ class UiAllScreensSanityTest {
@get:Rule
val testRule = RuleChain
.outerRule(ActivityScenarioRule(MainActivity::class.java))
+ .around(GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE))
.around(ScreenshotFailureRule())
private val elementRobot = ElementRobot()
@@ -94,6 +97,30 @@ class UiAllScreensSanityTest {
}
}
+ elementRobot.space {
+ createSpace {
+ crawl()
+ }
+ val spaceName = UUID.randomUUID().toString()
+ createSpace {
+ createPublicSpace(spaceName)
+ }
+
+ spaceMenu(spaceName) {
+ spaceMembers()
+ spaceSettings {
+ crawl()
+ }
+ exploreRooms()
+
+ invitePeople().also { openMenu(spaceName) }
+ addRoom().also { openMenu(spaceName) }
+ addSpace().also { openMenu(spaceName) }
+
+ leaveSpace()
+ }
+ }
+
elementRobot.withDeveloperMode {
settings {
advancedSettings { crawlDeveloperOptions() }
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
index a5962d16fe..f0ce23b7db 100644
--- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt
@@ -35,6 +35,7 @@ import im.vector.app.features.home.HomeActivity
import im.vector.app.features.onboarding.OnboardingActivity
import im.vector.app.initialSyncIdlingResource
import im.vector.app.ui.robot.settings.SettingsRobot
+import im.vector.app.ui.robot.space.SpaceRobot
import im.vector.app.withIdlingResource
import timber.log.Timber
@@ -147,6 +148,10 @@ class ElementRobot {
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
}.onFailure { Timber.w(it, "Verification popup missing") }
}
+
+ fun space(block: SpaceRobot.() -> Unit) {
+ block(SpaceRobot())
+ }
}
private fun Boolean.toWarningType() = if (this) "shown" else "skipped"
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceCreateRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceCreateRobot.kt
new file mode 100644
index 0000000000..68e5fa5059
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceCreateRobot.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2022 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.ui.robot.space
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.Espresso.pressBack
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.espresso.tools.waitUntilDialogVisible
+import im.vector.app.espresso.tools.waitUntilViewVisible
+import im.vector.app.features.home.HomeActivity
+import im.vector.app.features.spaces.manage.SpaceManageActivity
+import java.util.UUID
+
+class SpaceCreateRobot {
+
+ fun crawl() {
+ // public
+ clickOn(R.id.publicButton)
+ waitUntilViewVisible(withId(R.id.recyclerView))
+ onView(ViewMatchers.withHint(R.string.create_room_name_hint)).perform(ViewActions.replaceText(UUID.randomUUID().toString()))
+ clickOn(R.id.nextButton)
+ waitUntilViewVisible(withId(R.id.recyclerView))
+ pressBack()
+ pressBack()
+
+ // private
+ clickOn(R.id.privateButton)
+ waitUntilViewVisible(withId(R.id.recyclerView))
+ clickOn(R.id.nextButton)
+
+ waitUntilViewVisible(withId(R.id.teammatesButton))
+ // me and teammates
+ clickOn(R.id.teammatesButton)
+ waitUntilViewVisible(withId(R.id.recyclerView))
+ clickOn(R.id.nextButton)
+ pressBack()
+ pressBack()
+
+ // just me
+ waitUntilViewVisible(withId(R.id.justMeButton))
+ clickOn(R.id.justMeButton)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(withId(R.id.roomList))
+ }
+
+ onView(withId(R.id.roomList))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(withText(R.string.room_displayname_empty_room)),
+ click()
+ ).atPosition(0)
+ )
+ clickOn(R.id.spaceAddRoomSaveItem)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(withId(R.id.roomListContainer))
+ }
+ }
+
+ fun createPublicSpace(spaceName: String) {
+ clickOn(R.id.publicButton)
+ waitUntilViewVisible(withId(R.id.recyclerView))
+ onView(ViewMatchers.withHint(R.string.create_room_name_hint)).perform(ViewActions.replaceText(spaceName))
+ clickOn(R.id.nextButton)
+ waitUntilViewVisible(withId(R.id.recyclerView))
+ clickOn(R.id.nextButton)
+ waitUntilDialogVisible(withId(R.id.inviteByMxidButton))
+ // close invite dialog
+ pressBack()
+ waitUntilViewVisible(withId(R.id.timelineRecyclerView))
+ // close room
+ pressBack()
+ waitUntilViewVisible(withId(R.id.roomListContainer))
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt
new file mode 100644
index 0000000000..431df396d0
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2022 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.ui.robot.space
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.internal.viewaction.ClickChildAction
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.espresso.tools.waitUntilDialogVisible
+import im.vector.app.espresso.tools.waitUntilViewVisible
+import im.vector.app.features.invite.InviteUsersToRoomActivity
+import im.vector.app.features.roomprofile.RoomProfileActivity
+import im.vector.app.features.spaces.SpaceExploreActivity
+import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
+import im.vector.app.features.spaces.manage.SpaceManageActivity
+import org.hamcrest.Matchers
+
+class SpaceMenuRobot {
+
+ fun openMenu(spaceName: String) {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
+ onView(ViewMatchers.withId(R.id.groupListView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(Matchers.allOf(ViewMatchers.withId(R.id.groupNameView), ViewMatchers.withText(spaceName))),
+ ClickChildAction.clickChildWithId(R.id.groupTmpLeave)
+ ).atPosition(0)
+ )
+ waitUntilDialogVisible(ViewMatchers.withId(R.id.spaceNameView))
+ }
+
+ fun invitePeople() = apply {
+ clickOn(R.id.invitePeople)
+ waitUntilDialogVisible(ViewMatchers.withId(R.id.inviteByMxidButton))
+ clickOn(R.id.inviteByMxidButton)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.userListRecyclerView))
+ }
+ // close keyboard
+ Espresso.pressBack()
+ // close invite view
+ Espresso.pressBack()
+ }
+
+ fun spaceMembers() {
+ clickOn(R.id.showMemberList)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
+ }
+ Espresso.pressBack()
+ }
+
+ fun spaceSettings(block: SpaceSettingsRobot.() -> Unit) {
+ clickOn(R.id.spaceSettings)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
+ }
+ block(SpaceSettingsRobot())
+ }
+
+ fun exploreRooms() {
+ clickOn(R.id.exploreRooms)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.spaceDirectoryList))
+ }
+ Espresso.pressBack()
+ }
+
+ fun addRoom() = apply {
+ clickOn(R.id.addRooms)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
+ }
+ Espresso.pressBack()
+ }
+
+ fun addSpace() = apply {
+ clickOn(R.id.addSpaces)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
+ }
+ Espresso.pressBack()
+ }
+
+ fun leaveSpace() {
+ clickOn(R.id.leaveSpace)
+ waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton))
+ clickOn(R.id.leave_selected)
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
+ }
+ clickOn(R.id.spaceLeaveButton)
+ waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceRobot.kt
new file mode 100644
index 0000000000..ffb3c24051
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceRobot.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 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.ui.robot.space
+
+import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
+import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawer
+import im.vector.app.R
+
+class SpaceRobot {
+
+ fun createSpace(block: SpaceCreateRobot.() -> Unit) {
+ openDrawer()
+ clickOn(R.string.add_space)
+ block(SpaceCreateRobot())
+ }
+
+ fun spaceMenu(spaceName: String, block: SpaceMenuRobot.() -> Unit) {
+ openDrawer()
+ with(SpaceMenuRobot()) {
+ openMenu(spaceName)
+ block()
+ }
+ }
+}
diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceSettingsRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceSettingsRobot.kt
new file mode 100644
index 0000000000..dcd003da98
--- /dev/null
+++ b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceSettingsRobot.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2022 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.ui.robot.space
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.espresso.Espresso
+import androidx.test.espresso.action.ViewActions
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.ViewMatchers
+import im.vector.app.R
+import im.vector.app.espresso.tools.waitUntilActivityVisible
+import im.vector.app.espresso.tools.waitUntilViewVisible
+import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity
+
+class SpaceSettingsRobot {
+ fun crawl() {
+ Espresso.onView(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.room_settings_space_access_title)),
+ ViewActions.click()
+ )
+ )
+
+ waitUntilActivityVisible {
+ waitUntilViewVisible(ViewMatchers.withId(R.id.genericRecyclerView))
+ }
+
+ Espresso.pressBack()
+
+ Espresso.onView(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.space_settings_manage_rooms)),
+ ViewActions.click()
+ )
+ )
+
+ waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
+ Espresso.pressBack()
+
+ Espresso.onView(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.space_settings_permissions_title)),
+ ViewActions.click()
+ )
+ )
+
+ waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
+ Espresso.pressBack()
+ Espresso.pressBack()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt
index e64188765e..a3f4ffcfcd 100644
--- a/vector/src/main/java/im/vector/app/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/app/VectorApplication.kt
@@ -55,7 +55,6 @@ import im.vector.app.features.pin.PinLocker
import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.rageshake.VectorFileLogger
import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler
-import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.themes.ThemeUtils
@@ -63,7 +62,6 @@ import im.vector.app.features.version.VersionProvider
import im.vector.app.push.fcm.FcmHelper
import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler
import org.matrix.android.sdk.api.Matrix
-import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import timber.log.Timber
@@ -77,7 +75,6 @@ import androidx.work.Configuration as WorkConfiguration
@HiltAndroidApp
class VectorApplication :
Application(),
- MatrixConfiguration.Provider,
WorkConfiguration.Provider {
lateinit var appContext: Context
@@ -100,6 +97,7 @@ class VectorApplication :
@Inject lateinit var autoRageShaker: AutoRageShaker
@Inject lateinit var vectorFileLogger: VectorFileLogger
@Inject lateinit var vectorAnalytics: VectorAnalytics
+ @Inject lateinit var matrix: Matrix
// font thread handler
private var fontThreadHandler: Handler? = null
@@ -220,16 +218,9 @@ class VectorApplication :
}
}
- override fun providesMatrixConfiguration(): MatrixConfiguration {
- return MatrixConfiguration(
- applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
- roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(this)
- )
- }
-
override fun getWorkManagerConfiguration(): WorkConfiguration {
return WorkConfiguration.Builder()
- .setWorkerFactory(Matrix.getInstance(this.appContext).workerFactory())
+ .setWorkerFactory(matrix.workerFactory())
.setExecutor(Executors.newCachedThreadPool())
.build()
}
diff --git a/vector/src/main/java/im/vector/app/core/di/NamedGlobalScope.kt b/vector/src/main/java/im/vector/app/core/di/NamedGlobalScope.kt
new file mode 100644
index 0000000000..cc1ac829a1
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/di/NamedGlobalScope.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 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.core.di
+
+import javax.inject.Qualifier
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+annotation class NamedGlobalScope
diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
index 0e19cd4388..a5575ef536 100644
--- a/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/SingletonModule.kt
@@ -26,13 +26,16 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+import im.vector.app.BuildConfig
import im.vector.app.EmojiCompatWrapper
import im.vector.app.EmojiSpanify
+import im.vector.app.config.analyticsConfig
import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock
+import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
@@ -42,12 +45,15 @@ import im.vector.app.features.navigation.DefaultNavigator
import im.vector.app.features.navigation.Navigator
import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.SharedPrefPinCodeStore
+import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
import im.vector.app.features.ui.SharedPreferencesUiStateRepository
import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import org.matrix.android.sdk.api.Matrix
+import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
@@ -107,8 +113,17 @@ object VectorStaticModule {
}
@Provides
- fun providesMatrix(context: Context): Matrix {
- return Matrix.getInstance(context)
+ fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
+ return MatrixConfiguration(
+ applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
+ roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun providesMatrix(context: Context, configuration: MatrixConfiguration): Matrix {
+ return Matrix.createInstance(context, configuration)
}
@Provides
@@ -147,4 +162,16 @@ object VectorStaticModule {
fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default)
}
+
+ @Suppress("EXPERIMENTAL_API_USAGE")
+ @Provides
+ @NamedGlobalScope
+ fun providesGlobalScope(): CoroutineScope {
+ return GlobalScope
+ }
+
+ @Provides
+ fun providesAnalyticsConfig(): AnalyticsConfig {
+ return analyticsConfig
+ }
}
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
index 8164df9c55..5767acd44b 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt
@@ -67,7 +67,7 @@ import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.AnalyticsTracker
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.consent.ConsentNotGivenHelper
@@ -97,7 +97,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver
* Analytics
* ========================================================================================== */
- protected var analyticsScreenName: Screen.ScreenName? = null
+ protected var analyticsScreenName: MobileScreen.ScreenName? = null
private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
index 7e6a429274..869a12e871 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt
@@ -38,7 +38,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.analytics.AnalyticsTracker
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.screen.ScreenEvent
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -53,7 +53,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomShe
* Analytics
* ========================================================================================== */
- protected var analyticsScreenName: Screen.ScreenName? = null
+ protected var analyticsScreenName: MobileScreen.ScreenName? = null
private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker
diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
index 8a1b9051cc..6bd62707f2 100644
--- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt
@@ -44,7 +44,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.ToolbarConfig
import im.vector.app.features.analytics.AnalyticsTracker
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
@@ -58,7 +58,7 @@ abstract class VectorBaseFragment : Fragment(), MavericksView
* Analytics
* ========================================================================================== */
- protected var analyticsScreenName: Screen.ScreenName? = null
+ protected var analyticsScreenName: MobileScreen.ScreenName? = null
private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker
diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt
index a6f6d8d82f..8621c28d57 100644
--- a/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt
+++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncService.kt
@@ -35,6 +35,7 @@ import im.vector.app.R
import im.vector.app.core.platform.PendingIntentCompat
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.settings.BackgroundSyncMode
+import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.internal.session.sync.job.SyncService
import timber.log.Timber
import javax.inject.Inject
@@ -75,6 +76,9 @@ class VectorSyncService : SyncService() {
}
@Inject lateinit var notificationUtils: NotificationUtils
+ @Inject lateinit var matrix: Matrix
+
+ override fun provideMatrix() = matrix
override fun getDefaultSyncDelaySeconds() = BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS
diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
index 6dbf412d83..7b653ef44b 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/impl/DefaultVectorAnalytics.kt
@@ -16,19 +16,18 @@
package im.vector.app.features.analytics.impl
-import android.content.Context
import com.posthog.android.Options
import com.posthog.android.PostHog
import com.posthog.android.Properties
-import im.vector.app.BuildConfig
-import im.vector.app.config.analyticsConfig
+import im.vector.app.core.di.NamedGlobalScope
+import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.log.analyticsTag
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.analytics.store.AnalyticsStore
-import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -41,15 +40,30 @@ private val IGNORED_OPTIONS: Options? = null
@Singleton
class DefaultVectorAnalytics @Inject constructor(
- private val context: Context,
- private val analyticsStore: AnalyticsStore
+ postHogFactory: PostHogFactory,
+ analyticsConfig: AnalyticsConfig,
+ private val analyticsStore: AnalyticsStore,
+ private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
+ @NamedGlobalScope private val globalScope: CoroutineScope
) : VectorAnalytics {
- private var posthog: PostHog? = null
+
+ private val posthog: PostHog? = when {
+ analyticsConfig.isEnabled -> postHogFactory.createPosthog()
+ else -> {
+ Timber.tag(analyticsTag.value).w("Analytics is disabled")
+ null
+ }
+ }
// Cache for the store values
private var userConsent: Boolean? = null
private var analyticsId: String? = null
+ override fun init() {
+ observeUserConsent()
+ observeAnalyticsId()
+ }
+
override fun getUserConsent(): Flow {
return analyticsStore.userConsentFlow
}
@@ -82,13 +96,6 @@ class DefaultVectorAnalytics @Inject constructor(
setAnalyticsId("")
}
- override fun init() {
- observeUserConsent()
- observeAnalyticsId()
- createAnalyticsClient()
- }
-
- @Suppress("EXPERIMENTAL_API_USAGE")
private fun observeAnalyticsId() {
getAnalyticsId()
.onEach { id ->
@@ -96,21 +103,20 @@ class DefaultVectorAnalytics @Inject constructor(
analyticsId = id
identifyPostHog()
}
- .launchIn(GlobalScope)
+ .launchIn(globalScope)
}
- private fun identifyPostHog() {
+ private suspend fun identifyPostHog() {
val id = analyticsId ?: return
if (id.isEmpty()) {
Timber.tag(analyticsTag.value).d("reset")
posthog?.reset()
} else {
Timber.tag(analyticsTag.value).d("identify")
- posthog?.identify(id)
+ posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS)
}
}
- @Suppress("EXPERIMENTAL_API_USAGE")
private fun observeUserConsent() {
getUserConsent()
.onEach { consent ->
@@ -118,49 +124,13 @@ class DefaultVectorAnalytics @Inject constructor(
userConsent = consent
optOutPostHog()
}
- .launchIn(GlobalScope)
+ .launchIn(globalScope)
}
private fun optOutPostHog() {
userConsent?.let { posthog?.optOut(!it) }
}
- private fun createAnalyticsClient() {
- Timber.tag(analyticsTag.value).d("createAnalyticsClient()")
-
- if (analyticsConfig.isEnabled.not()) {
- Timber.tag(analyticsTag.value).w("Analytics is disabled")
- return
- }
-
- posthog = PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
- // Record certain application events automatically! (off/false by default)
- // .captureApplicationLifecycleEvents()
- // Record screen views automatically! (off/false by default)
- // .recordScreenViews()
- // Capture deep links as part of the screen call. (off by default)
- // .captureDeepLinks()
- // Maximum number of events to keep in queue before flushing (default 20)
- // .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()
-
- optOutPostHog()
- identifyPostHog()
- }
-
- private fun getLogLevel(): PostHog.LogLevel {
- return if (BuildConfig.DEBUG) {
- PostHog.LogLevel.DEBUG
- } else {
- PostHog.LogLevel.INFO
- }
- }
-
override fun capture(event: VectorAnalyticsEvent) {
Timber.tag(analyticsTag.value).d("capture($event)")
posthog
diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactory.kt
new file mode 100644
index 0000000000..d961ceaadc
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactory.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.impl
+
+import android.content.Context
+import im.vector.app.ActiveSessionDataSource
+import im.vector.app.core.extensions.vectorStore
+import im.vector.app.features.analytics.extensions.toTrackingValue
+import im.vector.app.features.analytics.plan.UserProperties
+import javax.inject.Inject
+
+class LateInitUserPropertiesFactory @Inject constructor(
+ private val activeSessionDataSource: ActiveSessionDataSource,
+ private val context: Context,
+) {
+ suspend fun createUserProperties(): UserProperties? {
+ val useCase = activeSessionDataSource.currentValue?.orNull()?.vectorStore(context)?.readUseCase()
+ return useCase?.let {
+ UserProperties(ftueUseCaseSelection = it.toTrackingValue())
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt b/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt
new file mode 100644
index 0000000000..029732f76c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/analytics/impl/PostHogFactory.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 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.impl
+
+import android.content.Context
+import com.posthog.android.PostHog
+import im.vector.app.BuildConfig
+import im.vector.app.config.analyticsConfig
+import javax.inject.Inject
+
+class PostHogFactory @Inject constructor(private val context: Context) {
+
+ fun createPosthog(): PostHog {
+ return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
+ // Record certain application events automatically! (off/false by default)
+ // .captureApplicationLifecycleEvents()
+ // Record screen views automatically! (off/false by default)
+ // .recordScreenViews()
+ // Capture deep links as part of the screen call. (off by default)
+ // .captureDeepLinks()
+ // Maximum number of events to keep in queue before flushing (default 20)
+ // .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 (BuildConfig.DEBUG) {
+ PostHog.LogLevel.DEBUG
+ } else {
+ PostHog.LogLevel.INFO
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt
similarity index 67%
rename from vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt
rename to vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt
index 710ae8f6f2..758a0540bf 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/plan/Screen.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/plan/MobileScreen.kt
@@ -22,9 +22,9 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
// https://github.com/matrix-org/matrix-analytics-events/
/**
- * Triggered when the user changed screen
+ * Triggered when the user changed screen on Element Android/iOS
*/
-data class Screen(
+data class MobileScreen(
/**
* How long the screen was displayed for in milliseconds.
*/
@@ -33,6 +33,11 @@ data class Screen(
) : VectorAnalyticsScreen {
enum class ScreenName {
+ /**
+ * The screen that displays the user's breadcrumbs.
+ */
+ Breadcrumbs,
+
/**
* The screen shown to create a new (non-direct) room.
*/
@@ -43,6 +48,16 @@ data class Screen(
*/
DeactivateAccount,
+ /**
+ * The tab on mobile that displays the dialpad.
+ */
+ Dialpad,
+
+ /**
+ * The Favourites tab on mobile that lists your favourite people/rooms.
+ */
+ Favourites,
+
/**
* The form for the forgot password use case
*/
@@ -54,11 +69,15 @@ data class Screen(
Group,
/**
- * The Home tab on iOS | possibly the same on Android? | Home page on
- * Web
+ * The Home tab on iOS | possibly the same on Android?
*/
Home,
+ /**
+ * The screen shown to share a link to download the app.
+ */
+ InviteFriends,
+
/**
* The screen that displays the login flow (when the user already has an
* account).
@@ -66,100 +85,14 @@ data class Screen(
Login,
/**
- * The screen that displays the user's breadcrumbs.
+ * Legacy: The screen that shows all groups/communities you have joined.
*/
- MobileBreadcrumbs,
-
- /**
- * The tab on mobile that displays the dialpad.
- */
- MobileDialpad,
-
- /**
- * The Favourites tab on mobile that lists your favourite people/rooms.
- */
- MobileFavourites,
-
- /**
- * The screen shown to share a link to download the app.
- */
- MobileInviteFriends,
+ MyGroups,
/**
* The People tab on mobile that lists all the DM rooms you have joined.
*/
- MobilePeople,
-
- /**
- * The Rooms tab on mobile that lists all the (non-direct) rooms you've
- * joined.
- */
- MobileRooms,
-
- /**
- * The Files tab shown in the global search screen on Mobile.
- */
- MobileSearchFiles,
-
- /**
- * The Messages tab shown in the global search screen on Mobile.
- */
- MobileSearchMessages,
-
- /**
- * The People tab shown in the global search screen on Mobile.
- */
- MobileSearchPeople,
-
- /**
- * The Rooms tab shown in the global search screen on Mobile.
- */
- MobileSearchRooms,
-
- /**
- * The global settings screen shown in the app.
- */
- MobileSettings,
-
- /**
- * The settings screen to change the default notification options.
- */
- MobileSettingsDefaultNotifications,
-
- /**
- * The settings screen to manage notification mentions and keywords.
- */
- MobileSettingsMentionsAndKeywords,
-
- /**
- * The global security settings screen.
- */
- MobileSettingsSecurity,
-
- /**
- * The sidebar shown on mobile with spaces, settings etc.
- */
- MobileSidebar,
-
- /**
- * Screen that displays the list of members of a space
- */
- MobileSpaceMembers,
-
- /**
- * The bottom sheet that list all space options
- */
- MobileSpaceMenu,
-
- /**
- * The screen shown to select which room directory you'd like to use.
- */
- MobileSwitchDirectory,
-
- /**
- * Legacy: The screen that shows all groups/communities you have joined.
- */
- MyGroups,
+ People,
/**
* The screen that displays the registration flow (when the user wants
@@ -216,107 +149,87 @@ data class Screen(
*/
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 settings screen to change the default notification options.
+ */
+ SettingsDefaultNotifications,
+
+ /**
+ * The settings screen to manage notification mentions and keywords.
+ */
+ SettingsMentionsAndKeywords,
+
+ /**
+ * The global security settings screen.
+ */
+ SettingsSecurity,
+
+ /**
+ * The sidebar shown on mobile with spaces, settings etc.
+ */
+ Sidebar,
+
/**
* 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,
+
/**
* A screen that shows information about a room member.
*/
User,
- /**
- * Element Web showing flow to trust this new device with cross-signing.
- */
- WebCompleteSecurity,
-
- /**
- * Element Web showing flow to setup SSSS / cross-signing on this
- * account.
- */
- WebE2ESetup,
-
- /**
- * Element Web loading spinner.
- */
- WebLoading,
-
- /**
- * Element Web device has been soft logged out by the server.
- */
- WebSoftLogout,
-
- /**
- * Legacy: Element Web User Settings Flair Tab.
- */
- WebUserSettingFlair,
-
- /**
- * Element Web User Settings Mjolnir (labs) Tab.
- */
- WebUserSettingMjolnir,
-
- /**
- * Element Web User Settings Appearance Tab.
- */
- WebUserSettingsAppearance,
-
- /**
- * Element Web User Settings General Tab.
- */
- WebUserSettingsGeneral,
-
- /**
- * Element Web User Settings Help & About Tab.
- */
- WebUserSettingsHelpAbout,
-
- /**
- * Element Web User Settings Ignored Users Tab.
- */
- WebUserSettingsIgnoredUsers,
-
- /**
- * Element Web User Settings Keyboard Tab.
- */
- WebUserSettingsKeyboard,
-
- /**
- * Element Web User Settings Labs Tab.
- */
- WebUserSettingsLabs,
-
- /**
- * Element Web User Settings Notifications Tab.
- */
- WebUserSettingsNotifications,
-
- /**
- * Element Web User Settings Preferences Tab.
- */
- WebUserSettingsPreferences,
-
- /**
- * Element Web User Settings Security & Privacy Tab.
- */
- WebUserSettingsSecurityPrivacy,
-
- /**
- * Element Web User Settings Sidebar Tab.
- */
- WebUserSettingsSidebar,
-
- /**
- * Element Web User Settings Voice & Video Tab.
- */
- WebUserSettingsVoiceVideo,
-
/**
* The splash screen.
*/
diff --git a/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt b/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt
index 8e0513f25a..1ad4a1fa32 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/screen/ScreenEvent.kt
@@ -18,13 +18,13 @@ package im.vector.app.features.analytics.screen
import android.os.SystemClock
import im.vector.app.features.analytics.AnalyticsTracker
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import timber.log.Timber
/**
* Track a screen display. Unique usage.
*/
-class ScreenEvent(val screenName: Screen.ScreenName) {
+class ScreenEvent(val screenName: MobileScreen.ScreenName) {
private val startTime = SystemClock.elapsedRealtime()
// Protection to avoid multiple sending
@@ -34,14 +34,14 @@ class ScreenEvent(val screenName: Screen.ScreenName) {
* @param screenNameOverride can be used to override the screen name passed in constructor parameter
*/
fun send(analyticsTracker: AnalyticsTracker,
- screenNameOverride: Screen.ScreenName? = null) {
+ screenNameOverride: MobileScreen.ScreenName? = null) {
if (isSent) {
Timber.w("Event $screenName Already sent!")
return
}
isSent = true
analyticsTracker.screen(
- Screen(
+ MobileScreen(
screenName = screenNameOverride ?: screenName,
durationMs = (SystemClock.elapsedRealtime() - startTime).toInt()
)
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt
new file mode 100644
index 0000000000..f287104415
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/AutocompleteHeaderItem.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 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.autocomplete
+
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.app.R
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.VectorEpoxyModel
+
+@EpoxyModelClass(layout = R.layout.item_autocomplete_header_item)
+abstract class AutocompleteHeaderItem : VectorEpoxyModel() {
+
+ @EpoxyAttribute var title: String? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.titleView.text = title
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val titleView by bind(R.id.headerItemAutocompleteTitle)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt
index 9b4bd78504..2034cee90a 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberController.kt
@@ -16,31 +16,81 @@
package im.vector.app.features.autocomplete.member
+import android.content.Context
import com.airbnb.epoxy.TypedEpoxyController
+import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener
+import im.vector.app.features.autocomplete.autocompleteHeaderItem
import im.vector.app.features.autocomplete.autocompleteMatrixItem
import im.vector.app.features.home.AvatarRenderer
-import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
-class AutocompleteMemberController @Inject constructor() : TypedEpoxyController>() {
+class AutocompleteMemberController @Inject constructor(private val context: Context) :
+ TypedEpoxyController>() {
- var listener: AutocompleteClickListener? = null
+ /* ==========================================================================================
+ * Fields
+ * ========================================================================================== */
+
+ var listener: AutocompleteClickListener? = null
+
+ /* ==========================================================================================
+ * Dependencies
+ * ========================================================================================== */
@Inject lateinit var avatarRenderer: AvatarRenderer
- override fun buildModels(data: List?) {
+ /* ==========================================================================================
+ * Specialization
+ * ========================================================================================== */
+
+ override fun buildModels(data: List?) {
if (data.isNullOrEmpty()) {
return
}
+ data.forEach { item ->
+ when (item) {
+ is AutocompleteMemberItem.Header -> buildHeaderItem(item)
+ is AutocompleteMemberItem.RoomMember -> buildRoomMemberItem(item)
+ is AutocompleteMemberItem.Everyone -> buildEveryoneItem(item)
+ }
+ }
+ }
+
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
+ private fun buildHeaderItem(header: AutocompleteMemberItem.Header) {
+ autocompleteHeaderItem {
+ id(header.id)
+ title(header.title)
+ }
+ }
+
+ private fun buildRoomMemberItem(roomMember: AutocompleteMemberItem.RoomMember) {
val host = this
- data.forEach { user ->
- autocompleteMatrixItem {
+ autocompleteMatrixItem {
+ roomMember.roomMemberSummary.let { user ->
id(user.userId)
matrixItem(user.toMatrixItem())
avatarRenderer(host.avatarRenderer)
- clickListener { host.listener?.onItemClick(user) }
+ clickListener { host.listener?.onItemClick(roomMember) }
+ }
+ }
+ }
+
+ private fun buildEveryoneItem(everyone: AutocompleteMemberItem.Everyone) {
+ val host = this
+ autocompleteMatrixItem {
+ everyone.roomSummary.let { room ->
+ id(room.roomId)
+ matrixItem(room.toEveryoneInRoomMatrixItem())
+ subName(host.context.getString(R.string.room_message_notify_everyone))
+ avatarRenderer(host.avatarRenderer)
+ clickListener { host.listener?.onItemClick(everyone) }
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberItem.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberItem.kt
new file mode 100644
index 0000000000..77c5069938
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberItem.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 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.autocomplete.member
+
+import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+
+sealed class AutocompleteMemberItem {
+ data class Header(val id: String, val title: String) : AutocompleteMemberItem()
+ data class RoomMember(val roomMemberSummary: RoomMemberSummary) : AutocompleteMemberItem()
+ data class Everyone(val roomSummary: RoomSummary) : AutocompleteMemberItem()
+}
diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt
index 4976cb39b9..ce3b9c6a7e 100644
--- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt
+++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt
@@ -21,26 +21,44 @@ import androidx.recyclerview.widget.RecyclerView
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter
+import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.api.util.MatrixItem
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
@Assisted val roomId: String,
- session: Session,
+ private val session: Session,
private val controller: AutocompleteMemberController
-) : RecyclerViewPresenter(context), AutocompleteClickListener {
+) : RecyclerViewPresenter(context), AutocompleteClickListener {
+
+ /* ==========================================================================================
+ * Fields
+ * ========================================================================================== */
private val room by lazy { session.getRoom(roomId)!! }
+ /* ==========================================================================================
+ * Init
+ * ========================================================================================== */
+
init {
controller.listener = this
}
+ /* ==========================================================================================
+ * Public api
+ * ========================================================================================== */
+
fun clear() {
controller.listener = null
}
@@ -50,29 +68,100 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
fun create(roomId: String): AutocompleteMemberPresenter
}
+ /* ==========================================================================================
+ * Specialization
+ * ========================================================================================== */
+
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return controller.adapter
}
- override fun onItemClick(t: RoomMemberSummary) {
+ override fun onItemClick(t: AutocompleteMemberItem) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
- val queryParams = roomMemberQueryParams {
- displayName = if (query.isNullOrBlank()) {
- QueryStringValue.IsNotEmpty
- } else {
- QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+ val queryParams = createQueryParams(query)
+ val membersHeader = createMembersHeader()
+ val members = createMemberItems(queryParams)
+ val everyone = createEveryoneItem(query)
+ // add headers only when user can notify everyone
+ val canAddHeaders = canNotifyEveryone()
+
+ val items = mutableListOf().apply {
+ if (members.isNotEmpty()) {
+ if (canAddHeaders) {
+ add(membersHeader)
+ }
+ addAll(members)
+ }
+ everyone?.let {
+ val everyoneHeader = createEveryoneHeader()
+ add(everyoneHeader)
+ add(it)
}
- memberships = listOf(Membership.JOIN)
- excludeSelf = true
}
- val members = room.getRoomMembers(queryParams)
- .asSequence()
- .sortedBy { it.displayName }
- .disambiguate()
- controller.setData(members.toList())
+
+ controller.setData(items)
+ }
+
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
+ private fun createQueryParams(query: CharSequence?) = roomMemberQueryParams {
+ displayName = if (query.isNullOrBlank()) {
+ QueryStringValue.IsNotEmpty
+ } else {
+ QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
+ }
+ memberships = listOf(Membership.JOIN)
+ excludeSelf = true
+ }
+
+ private fun createMembersHeader() =
+ AutocompleteMemberItem.Header(
+ ID_HEADER_MEMBERS,
+ context.getString(R.string.room_message_autocomplete_users)
+ )
+
+ private fun createMemberItems(queryParams: RoomMemberQueryParams) =
+ room.getRoomMembers(queryParams)
+ .asSequence()
+ .sortedBy { it.displayName }
+ .disambiguate()
+ .map { AutocompleteMemberItem.RoomMember(it) }
+ .toList()
+
+ private fun createEveryoneHeader() =
+ AutocompleteMemberItem.Header(
+ ID_HEADER_EVERYONE,
+ context.getString(R.string.room_message_autocomplete_notification)
+ )
+
+ private fun createEveryoneItem(query: CharSequence?) =
+ room.roomSummary()
+ ?.takeIf { canNotifyEveryone() }
+ ?.takeIf { query.isNullOrBlank() || MatrixItem.NOTIFY_EVERYONE.startsWith("@$query") }
+ ?.let {
+ AutocompleteMemberItem.Everyone(it)
+ }
+
+ private fun canNotifyEveryone() = session.resolveSenderNotificationPermissionCondition(
+ Event(
+ senderId = session.myUserId,
+ roomId = roomId
+ ),
+ SenderNotificationPermissionCondition(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY)
+ )
+
+ /* ==========================================================================================
+ * Const
+ * ========================================================================================== */
+
+ companion object {
+ private const val ID_HEADER_MEMBERS = "ID_HEADER_MEMBERS"
+ private const val ID_HEADER_EVERYONE = "ID_HEADER_EVERYONE"
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
index caab4c85e1..23c7b79914 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
@@ -54,6 +54,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.ActivityCallBinding
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
import im.vector.app.features.call.dialpad.DialPadFragment
+import im.vector.app.features.call.transfer.CallTransferActivity
import im.vector.app.features.call.utils.EglUtils
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
@@ -165,6 +166,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
?.let {
callViewModel.handle(VectorCallViewActions.SwitchCall(it))
}
+ this.intent = intent
}
override fun getMenuRes() = R.menu.vector_call
@@ -522,14 +524,21 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
val callId = withState(callViewModel) { it.callId }
navigator.openCallTransfer(this, callTransferActivityResultLauncher, callId)
}
+ is VectorCallViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
null -> {
}
}
}
private val callTransferActivityResultLauncher = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_CANCELED) {
- callViewModel.handle(VectorCallViewActions.CallTransferSelectionCancelled)
+ when (activityResult.resultCode) {
+ Activity.RESULT_CANCELED -> {
+ callViewModel.handle(VectorCallViewActions.CallTransferSelectionCancelled)
+ }
+ Activity.RESULT_OK -> {
+ CallTransferActivity.getCallTransferResult(activityResult.data)
+ ?.let { callViewModel.handle(VectorCallViewActions.CallTransferSelectionResult(it)) }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
index fb39660282..d1ed961814 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
@@ -18,6 +18,7 @@ package im.vector.app.features.call
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.call.audio.CallAudioManager
+import im.vector.app.features.call.transfer.CallTransferResult
sealed class VectorCallViewActions : VectorViewModelAction {
object EndCall : VectorCallViewActions()
@@ -37,5 +38,6 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object ToggleHDSD : VectorCallViewActions()
object InitiateCallTransfer : VectorCallViewActions()
object CallTransferSelectionCancelled : VectorCallViewActions()
+ data class CallTransferSelectionResult(val callTransferResult: CallTransferResult) : VectorCallViewActions()
object TransferCall : VectorCallViewActions()
}
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt
index 5a0a2f127c..7c29d7eea3 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt
@@ -29,6 +29,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
) : VectorCallViewEvents()
object ShowDialPad : VectorCallViewEvents()
object ShowCallTransferScreen : VectorCallViewEvents()
+ object FailToTransfer : VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : VectorCallViewEvents()
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
index 4aca0ea499..a26eec04f3 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
@@ -29,13 +29,17 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.audio.CallAudioManager
+import im.vector.app.features.call.dialpad.DialPadLookup
+import im.vector.app.features.call.transfer.CallTransferResult
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem
+import im.vector.app.features.createdirect.DirectRoomHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
@@ -47,7 +51,9 @@ class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState,
val session: Session,
val callManager: WebRtcCallManager,
- val proximityManager: CallProximityManager
+ val proximityManager: CallProximityManager,
+ private val dialPadLookup: DialPadLookup,
+ private val directRoomHelper: DirectRoomHelper,
) : VectorViewModel(initialState) {
private var call: WebRtcCall? = null
@@ -327,6 +333,9 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewActions.CallTransferSelectionCancelled -> {
call?.updateRemoteOnHold(false)
}
+ is VectorCallViewActions.CallTransferSelectionResult -> {
+ handleCallTransferSelectionResult(action.callTransferResult)
+ }
VectorCallViewActions.TransferCall -> {
handleCallTransfer()
}
@@ -345,6 +354,53 @@ class VectorCallViewModel @AssistedInject constructor(
}
}
+ private fun handleCallTransferSelectionResult(result: CallTransferResult) {
+ when (result) {
+ is CallTransferResult.ConnectWithUserId -> connectWithUserId(result)
+ is CallTransferResult.ConnectWithPhoneNumber -> connectWithPhoneNumber(result)
+ }.exhaustive
+ }
+
+ private fun connectWithUserId(result: CallTransferResult.ConnectWithUserId) {
+ viewModelScope.launch {
+ try {
+ if (result.consultFirst) {
+ val dmRoomId = directRoomHelper.ensureDMExists(result.selectedUserId)
+ callManager.startOutgoingCall(
+ nativeRoomId = dmRoomId,
+ otherUserId = result.selectedUserId,
+ isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
+ transferee = call
+ )
+ } else {
+ call?.transferToUser(result.selectedUserId, null)
+ }
+ } catch (failure: Throwable) {
+ _viewEvents.post(VectorCallViewEvents.FailToTransfer)
+ }
+ }
+ }
+
+ private fun connectWithPhoneNumber(action: CallTransferResult.ConnectWithPhoneNumber) {
+ viewModelScope.launch {
+ try {
+ val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
+ if (action.consultFirst) {
+ callManager.startOutgoingCall(
+ nativeRoomId = result.roomId,
+ otherUserId = result.userId,
+ isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
+ transferee = call
+ )
+ } else {
+ call?.transferToUser(result.userId, result.roomId)
+ }
+ } catch (failure: Throwable) {
+ _viewEvents.post(VectorCallViewEvents.FailToTransfer)
+ }
+ }
+ }
+
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory {
override fun create(initialState: VectorCallViewState): VectorCallViewModel
diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt
index 5fc866a4dd..b33ce25f55 100644
--- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt
@@ -40,7 +40,7 @@ import com.android.dialer.dialpadview.DigitsEditText
import im.vector.app.R
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.features.analytics.AnalyticsTracker
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.themes.ThemeUtils
@@ -69,7 +69,7 @@ class DialPadFragment : Fragment(), TextWatcher {
private var screenEvent: ScreenEvent? = null
override fun onResume() {
super.onResume()
- screenEvent = ScreenEvent(Screen.ScreenName.MobileDialpad)
+ screenEvent = ScreenEvent(MobileScreen.ScreenName.Dialpad)
}
override fun onPause() {
diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
index 0e63316bbe..d8eede6a55 100644
--- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
@@ -16,7 +16,6 @@
package im.vector.app.features.call.transfer
-import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
@@ -27,6 +26,7 @@ import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
+import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityCallTransferBinding
import kotlinx.parcelize.Parcelize
@@ -56,10 +56,8 @@ class CallTransferActivity : VectorBaseActivity() {
callTransferViewModel.observeViewEvents {
when (it) {
- is CallTransferViewEvents.Complete -> handleComplete()
- CallTransferViewEvents.Loading -> showWaitingView()
- is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
- }
+ is CallTransferViewEvents.Complete -> handleComplete()
+ }.exhaustive
}
sectionsPagerAdapter = CallTransferPagerAdapter(this)
@@ -82,29 +80,41 @@ class CallTransferActivity : VectorBaseActivity() {
when (views.callTransferTabLayout.selectedTabPosition) {
CallTransferPagerAdapter.USER_LIST_INDEX -> {
val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull() ?: return@debouncedClicks
- val action = CallTransferAction.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
- callTransferViewModel.handle(action)
+ val result = CallTransferResult.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
+ handleComplete(result)
}
CallTransferPagerAdapter.DIAL_PAD_INDEX -> {
val phoneNumber = sectionsPagerAdapter.dialPadFragment?.getRawInput() ?: return@debouncedClicks
- val action = CallTransferAction.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
- callTransferViewModel.handle(action)
+ val result = CallTransferResult.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
+ handleComplete(result)
}
}
}
}
- private fun handleComplete() {
- setResult(Activity.RESULT_OK)
+ private fun handleComplete(callTransferResult: CallTransferResult? = null) {
+ if (callTransferResult != null) {
+ val intent = Intent().apply {
+ putExtra(EXTRA_TRANSFER_RESULT, callTransferResult)
+ }
+ setResult(RESULT_OK, intent)
+ } else {
+ setResult(RESULT_OK)
+ }
finish()
}
companion object {
+ private const val EXTRA_TRANSFER_RESULT = "EXTRA_TRANSFER_RESULT"
fun newIntent(context: Context, callId: String): Intent {
return Intent(context, CallTransferActivity::class.java).also {
it.putExtra(Mavericks.KEY_ARG, CallTransferArgs(callId))
}
}
+
+ fun getCallTransferResult(intent: Intent?): CallTransferResult? {
+ return intent?.extras?.getParcelable(EXTRA_TRANSFER_RESULT)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferResult.kt
similarity index 64%
rename from vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt
rename to vector/src/main/java/im/vector/app/features/call/transfer/CallTransferResult.kt
index bd694ad14e..0629e91d35 100644
--- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt
+++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferResult.kt
@@ -16,9 +16,10 @@
package im.vector.app.features.call.transfer
-import im.vector.app.core.platform.VectorViewModelAction
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
-sealed class CallTransferAction : VectorViewModelAction {
- data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
- data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction()
+sealed class CallTransferResult : Parcelable {
+ @Parcelize data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferResult()
+ @Parcelize data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferResult()
}
diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt
index a8451e4fb5..4202506d23 100644
--- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt
@@ -20,6 +20,4 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class CallTransferViewEvents : VectorViewEvents {
object Complete : CallTransferViewEvents()
- object Loading : CallTransferViewEvents()
- object FailToTransfer : CallTransferViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt
index de6a5de539..1765b58a02 100644
--- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt
@@ -22,22 +22,16 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
-import im.vector.app.core.extensions.exhaustive
+import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.VectorViewModel
-import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
-import im.vector.app.features.createdirect.DirectRoomHelper
-import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
- private val dialPadLookup: DialPadLookup,
- private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager) :
- VectorViewModel(initialState) {
+ VectorViewModel(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory {
@@ -68,53 +62,5 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
call?.removeListener(callListener)
}
- override fun handle(action: CallTransferAction) {
- when (action) {
- is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
- is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
- }.exhaustive
- }
-
- private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
- viewModelScope.launch {
- try {
- if (action.consultFirst) {
- val dmRoomId = directRoomHelper.ensureDMExists(action.selectedUserId)
- callManager.startOutgoingCall(
- nativeRoomId = dmRoomId,
- otherUserId = action.selectedUserId,
- isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
- transferee = call
- )
- } else {
- call?.transferToUser(action.selectedUserId, null)
- }
- _viewEvents.post(CallTransferViewEvents.Complete)
- } catch (failure: Throwable) {
- _viewEvents.post(CallTransferViewEvents.FailToTransfer)
- }
- }
- }
-
- private fun connectWithPhoneNumber(action: CallTransferAction.ConnectWithPhoneNumber) {
- viewModelScope.launch {
- try {
- _viewEvents.post(CallTransferViewEvents.Loading)
- val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
- if (action.consultFirst) {
- callManager.startOutgoingCall(
- nativeRoomId = result.roomId,
- otherUserId = result.userId,
- isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
- transferee = call
- )
- } else {
- call?.transferToUser(result.userId, result.roomId)
- }
- _viewEvents.post(CallTransferViewEvents.Complete)
- } catch (failure: Throwable) {
- _viewEvents.post(CallTransferViewEvents.FailToTransfer)
- }
- }
- }
+ override fun handle(action: EmptyAction) { }
}
diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
index 2d93bab6a3..a6b34eda25 100644
--- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt
@@ -43,7 +43,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.core.utils.registerForPermissionsResult
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.qrcode.QrCodeScannerEvents
import im.vector.app.features.qrcode.QrCodeScannerFragment
@@ -71,7 +71,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.StartChat
+ analyticsScreenName = MobileScreen.ScreenName.StartChat
views.toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
index 6b6be63480..f6b32973a0 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt
@@ -48,7 +48,7 @@ import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet
@@ -165,7 +165,7 @@ class HomeActivity :
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
private var drawerScreenEvent: ScreenEvent? = null
override fun onDrawerOpened(drawerView: View) {
- drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileSidebar)
+ drawerScreenEvent = ScreenEvent(MobileScreen.ScreenName.Sidebar)
}
override fun onDrawerClosed(drawerView: View) {
@@ -184,7 +184,7 @@ class HomeActivity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.Home
+ analyticsScreenName = MobileScreen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
index a07409d063..ea03b833ac 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
@@ -457,7 +457,7 @@ class HomeDetailFragment @Inject constructor(
backgroundColor = if (highlight) {
ThemeUtils.getColor(requireContext(), R.attr.colorError)
} else {
- ThemeUtils.getColor(requireContext(), R.attr.vctr_unread_room_badge)
+ ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt
index 9af06ef801..1aee0257f4 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt
@@ -30,7 +30,7 @@ import im.vector.app.core.extensions.replaceChildFragment
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentHomeDrawerBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.spaces.SpaceListFragment
@@ -98,7 +98,7 @@ class HomeDrawerFragment @Inject constructor(
views.homeDrawerInviteFriendButton.debouncedClicks {
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
- analyticsTracker.screen(Screen(screenName = Screen.ScreenName.MobileInviteFriends))
+ analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends))
val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
index 9f85d4015b..be5f9c0bb4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/AutoCompleter.kt
@@ -33,6 +33,7 @@ import im.vector.app.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.app.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter
import im.vector.app.features.autocomplete.group.AutocompleteGroupPresenter
+import im.vector.app.features.autocomplete.member.AutocompleteMemberItem
import im.vector.app.features.autocomplete.member.AutocompleteMemberPresenter
import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.app.features.command.Command
@@ -41,9 +42,9 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.html.PillImageSpan
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.group.model.GroupSummary
-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.util.MatrixItem
+import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
@@ -106,7 +107,7 @@ class AutoCompleter @AssistedInject constructor(
Autocomplete.on(editText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
@@ -125,15 +126,24 @@ class AutoCompleter @AssistedInject constructor(
private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
- Autocomplete.on(editText)
- .with(CharPolicy('@', true))
+ Autocomplete.on(editText)
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_MEMBERS, true))
.with(autocompleteMemberPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
- .with(object : AutocompleteCallback {
- override fun onPopupItemClicked(editable: Editable, item: RoomMemberSummary): Boolean {
- insertMatrixItem(editText, editable, "@", item.toMatrixItem())
- return true
+ .with(object : AutocompleteCallback {
+ override fun onPopupItemClicked(editable: Editable, item: AutocompleteMemberItem): Boolean {
+ return when (item) {
+ is AutocompleteMemberItem.Header -> false // do nothing header is not clickable
+ is AutocompleteMemberItem.RoomMember -> {
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomMemberSummary.toMatrixItem())
+ true
+ }
+ is AutocompleteMemberItem.Everyone -> {
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomSummary.toEveryoneInRoomMatrixItem())
+ true
+ }
+ }
}
override fun onPopupVisibilityChanged(shown: Boolean) {
@@ -144,13 +154,13 @@ class AutoCompleter @AssistedInject constructor(
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
Autocomplete.on(editText)
- .with(CharPolicy('#', true))
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_ROOMS, true))
.with(autocompleteRoomPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
- insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_ROOMS, item.toRoomAliasMatrixItem())
return true
}
@@ -162,13 +172,13 @@ class AutoCompleter @AssistedInject constructor(
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
Autocomplete.on(editText)
- .with(CharPolicy('+', true))
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_GROUPS, true))
.with(autocompleteGroupPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
- insertMatrixItem(editText, editable, "+", item.toMatrixItem())
+ insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_GROUPS, item.toMatrixItem())
return true
}
@@ -180,9 +190,9 @@ class AutoCompleter @AssistedInject constructor(
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
Autocomplete.on(editText)
- .with(CharPolicy(':', false))
+ .with(CharPolicy(TRIGGER_AUTO_COMPLETE_EMOJIS, false))
.with(autocompleteEmojiPresenter)
- .with(ELEVATION)
+ .with(ELEVATION_DP)
.with(backgroundDrawable)
.with(object : AutocompleteCallback {
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
@@ -210,7 +220,7 @@ class AutoCompleter @AssistedInject constructor(
.build()
}
- private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
+ private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: Char, matrixItem: MatrixItem) {
// Detect last firstChar and remove it
var startIndex = editable.lastIndexOf(firstChar)
if (startIndex == -1) {
@@ -228,7 +238,7 @@ class AutoCompleter @AssistedInject constructor(
// Adding trailing space " " or ": " if the user started mention someone
val displayNameSuffix =
- if (firstChar == "@" && startIndex == 0) {
+ if (matrixItem is MatrixItem.UserItem) {
": "
} else {
" "
@@ -249,6 +259,10 @@ class AutoCompleter @AssistedInject constructor(
}
companion object {
- private const val ELEVATION = 6f
+ private const val ELEVATION_DP = 6f
+ private const val TRIGGER_AUTO_COMPLETE_MEMBERS = '@'
+ private const val TRIGGER_AUTO_COMPLETE_ROOMS = '#'
+ private const val TRIGGER_AUTO_COMPLETE_GROUPS = '+'
+ private const val TRIGGER_AUTO_COMPLETE_EMOJIS = ':'
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
index ae24052aa2..f5bf086e96 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt
@@ -35,7 +35,7 @@ import im.vector.app.core.extensions.keepScreenOn
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityRoomDetailBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.screen.ScreenEvent
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
@@ -160,7 +160,7 @@ class RoomDetailActivity :
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
private var drawerScreenEvent: ScreenEvent? = null
override fun onDrawerOpened(drawerView: View) {
- drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileBreadcrumbs)
+ drawerScreenEvent = ScreenEvent(MobileScreen.ScreenName.Breadcrumbs)
}
override fun onDrawerClosed(drawerView: View) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
index 86240a5ffe..d08a27324c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt
@@ -29,7 +29,7 @@ import java.io.File
* Transient events for RoomDetail
*/
sealed class RoomDetailViewEvents : VectorViewEvents {
- data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
+ data class Failure(val throwable: Throwable, val showInDialog: Boolean = false) : RoomDetailViewEvents()
data class OnNewTimelineEvents(val eventIds: List) : RoomDetailViewEvents()
data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
index 22d5fc2a77..71a299e11b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
@@ -49,6 +49,7 @@ data class JitsiState(
data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
+ val isInviteAlreadyAccepted: Boolean,
val myRoomMember: Async = Uninitialized,
val asyncInviter: Async = Uninitialized,
val asyncRoomSummary: Async = Uninitialized,
@@ -77,6 +78,7 @@ data class RoomDetailViewState(
constructor(args: TimelineArgs) : this(
roomId = args.roomId,
eventId = args.eventId,
+ isInviteAlreadyAccepted = args.isInviteAlreadyAccepted,
// Also highlight the target event, if any
highlightedEventId = args.eventId,
switchToParentSpace = args.switchToParentSpace,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 92319fabdc..b6cbd538f3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -120,7 +120,7 @@ import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogReportContentBinding
import im.vector.app.databinding.FragmentTimelineBinding
import im.vector.app.features.analytics.plan.Composer
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.attachments.AttachmentTypeSelectorView
import im.vector.app.features.attachments.AttachmentsHelper
import im.vector.app.features.attachments.ContactAttachment
@@ -342,7 +342,7 @@ class TimelineFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.Room
+ analyticsScreenName = MobileScreen.ScreenName.Room
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
timelineViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
@@ -448,7 +448,7 @@ class TimelineFragment @Inject constructor(
timelineViewModel.observeViewEvents {
when (it) {
- is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
+ is RoomDetailViewEvents.Failure -> displayErrorMessage(it)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
@@ -623,6 +623,10 @@ class TimelineFragment @Inject constructor(
)
}
+ private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) {
+ if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable)
+ }
+
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
val tag = RoomWidgetPermissionBottomSheet::class.java.name
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
@@ -2374,12 +2378,10 @@ class TimelineFragment @Inject constructor(
// VectorInviteView.Callback
override fun onAcceptInvite() {
- notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
timelineViewModel.handle(RoomDetailAction.AcceptInvite)
}
override fun onRejectInvite() {
- notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
timelineViewModel.handle(RoomDetailAction.RejectInvite)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index a831332407..628dccfade 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandle
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper
+import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorDataStore
@@ -123,6 +124,7 @@ class TimelineViewModel @AssistedInject constructor(
private val analyticsTracker: AnalyticsTracker,
private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val decryptionFailureTracker: DecryptionFailureTracker,
+ private val notificationDrawerManager: NotificationDrawerManager,
timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler
) : VectorViewModel(initialState),
@@ -193,6 +195,11 @@ class TimelineViewModel @AssistedInject constructor(
prepareForEncryption()
}
+ // If the user had already accepted the invitation in the room list
+ if (initialState.isInviteAlreadyAccepted) {
+ handleAcceptInvite()
+ }
+
if (initialState.switchToParentSpace) {
// We are coming from a notification, try to switch to the most relevant space
// so that when hitting back the room will appear in the list
@@ -803,16 +810,24 @@ class TimelineViewModel @AssistedInject constructor(
}
private fun handleRejectInvite() {
+ notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch {
- tryOrNull { session.leaveRoom(room.roomId) }
+ try {
+ session.leaveRoom(room.roomId)
+ } catch (throwable: Throwable) {
+ _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
+ }
}
}
private fun handleAcceptInvite() {
+ notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch {
- tryOrNull {
+ try {
session.joinRoom(room.roomId)
analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom())
+ } catch (throwable: Throwable) {
+ _viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt
index f22fe1b7df..a21567acb1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/arguments/TimelineArgs.kt
@@ -28,5 +28,6 @@ data class TimelineArgs(
val sharedData: SharedData? = null,
val openShareSpaceForId: String? = null,
val threadTimelineArgs: ThreadTimelineArgs? = null,
- val switchToParentSpace: Boolean = false
+ val switchToParentSpace: Boolean = false,
+ val isInviteAlreadyAccepted: Boolean = false
) : Parcelable
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
index 99a026a0cf..76ed024370 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.factory
+import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.prevOrNull
import im.vector.app.features.home.AvatarRenderer
@@ -26,10 +27,10 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisi
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
-import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem
-import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
+import im.vector.app.features.home.room.detail.timeline.item.MergedSimilarEventsItem
+import im.vector.app.features.home.room.detail.timeline.item.MergedSimilarEventsItem_
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
@@ -82,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
- callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
+ callback: TimelineEventController.Callback?): MergedSimilarEventsItem_? {
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
items,
currentPosition,
@@ -122,23 +123,31 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
- val attributes = MergedMembershipEventsItem.Attributes(
- isCollapsed = isCollapsed,
- mergeData = mergedData,
- avatarRenderer = avatarRenderer,
- onCollapsedStateChanged = {
- mergeItemCollapseStates[event.localId] = it
- requestModelBuild()
- }
- )
- MergedMembershipEventsItem_()
- .id(mergeId)
- .leftGuideline(avatarSizeProvider.leftGuideline)
- .highlighted(isCollapsed && highlighted)
- .attributes(attributes)
- .also {
- it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
- }
+ val summaryTitleResId = when (event.root.getClearType()) {
+ EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes
+ EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes
+ else -> null
+ }
+ summaryTitleResId?.let { summaryTitle ->
+ val attributes = MergedSimilarEventsItem.Attributes(
+ summaryTitleResId = summaryTitle,
+ isCollapsed = isCollapsed,
+ mergeData = mergedData,
+ avatarRenderer = avatarRenderer,
+ onCollapsedStateChanged = {
+ mergeItemCollapseStates[event.localId] = it
+ requestModelBuild()
+ }
+ )
+ MergedSimilarEventsItem_()
+ .id(mergeId)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ .highlighted(isCollapsed && highlighted)
+ .attributes(attributes)
+ .also {
+ it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
+ }
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 0c836748c8..aa1758dd6c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -61,6 +61,7 @@ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_
+import im.vector.app.features.home.room.detail.timeline.render.EventTextRenderer
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.EventHtmlRenderer
@@ -112,6 +113,7 @@ class MessageItemFactory @Inject constructor(
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy,
private val htmlCompressor: VectorHtmlCompressor,
+ private val textRendererFactory: EventTextRenderer.Factory,
private val stringProvider: StringProvider,
private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory,
@@ -138,6 +140,10 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId)
}
+ private val textRenderer by lazy {
+ textRendererFactory.create(roomId)
+ }
+
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val highlight = params.isHighlighted
@@ -549,8 +555,9 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
- val bindingOptions = spanUtils.getBindingOptions(body)
- val linkifiedBody = body.linkify(callback)
+ val renderedBody = textRenderer.render(body)
+ val bindingOptions = spanUtils.getBindingOptions(renderedBody)
+ val linkifiedBody = renderedBody.linkify(callback)
return MessageTextItem_()
.message(
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index bcccbc9f7c..53a9fbbaea 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -56,7 +56,8 @@ object TimelineDisplayableEvents {
}
fun TimelineEvent.canBeMerged(): Boolean {
- return root.getClearType() == EventType.STATE_ROOM_MEMBER
+ return root.getClearType() == EventType.STATE_ROOM_MEMBER ||
+ root.getClearType() == EventType.STATE_ROOM_SERVER_ACL
}
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedSimilarEventsItem.kt
similarity index 91%
rename from vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt
rename to vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedSimilarEventsItem.kt
index e19dc33fff..f012ca6cdc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedSimilarEventsItem.kt
@@ -20,6 +20,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
+import androidx.annotation.PluralsRes
import androidx.core.view.children
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@@ -27,7 +28,7 @@ import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
-abstract class MergedMembershipEventsItem : BasedMergedItem() {
+abstract class MergedSimilarEventsItem : BasedMergedItem() {
override fun getViewStubId() = STUB_ID
@@ -37,7 +38,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem,
override val avatarRenderer: AvatarRenderer,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt
new file mode 100644
index 0000000000..d50a6fb297
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/render/EventTextRenderer.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 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.home.room.detail.timeline.render
+
+import android.content.Context
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.core.glide.GlideApp
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.html.PillImageSpan
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.util.MatrixItem
+
+class EventTextRenderer @AssistedInject constructor(@Assisted private val roomId: String?,
+ private val context: Context,
+ private val avatarRenderer: AvatarRenderer,
+ private val sessionHolder: ActiveSessionHolder) {
+
+ /* ==========================================================================================
+ * Public api
+ * ========================================================================================== */
+
+ @AssistedFactory
+ interface Factory {
+ fun create(roomId: String?): EventTextRenderer
+ }
+
+ /**
+ * @param text the text you want to render
+ */
+ fun render(text: CharSequence): CharSequence {
+ return if (roomId != null && text.contains(MatrixItem.NOTIFY_EVERYONE)) {
+ SpannableStringBuilder(text).apply {
+ addNotifyEveryoneSpans(this, roomId)
+ }
+ } else {
+ text
+ }
+ }
+
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
+ private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) {
+ val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomId)
+ val matrixItem = MatrixItem.EveryoneInRoomItem(
+ id = roomId,
+ avatarUrl = room?.avatarUrl,
+ roomDisplayName = room?.displayName
+ )
+
+ // search for notify everyone text
+ var foundIndex = text.indexOf(MatrixItem.NOTIFY_EVERYONE, 0)
+ while (foundIndex >= 0) {
+ val endSpan = foundIndex + MatrixItem.NOTIFY_EVERYONE.length
+ addPillSpan(text, createPillImageSpan(matrixItem), foundIndex, endSpan)
+ foundIndex = text.indexOf(MatrixItem.NOTIFY_EVERYONE, endSpan)
+ }
+ }
+
+ private fun createPillImageSpan(matrixItem: MatrixItem) =
+ PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
+
+ private fun addPillSpan(
+ renderedText: Spannable,
+ pillSpan: PillImageSpan,
+ startSpan: Int,
+ endSpan: Int
+ ) {
+ renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt
index 0e16b4b0df..cf7c4a0e80 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/filtered/FilteredRoomsActivity.kt
@@ -24,7 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityFilteredRoomsBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.RoomListParams
@@ -43,7 +43,7 @@ class FilteredRoomsActivity : VectorBaseActivity()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.RoomFilter
+ analyticsScreenName = MobileScreen.ScreenName.RoomFilter
setupToolbar(views.filteredRoomsToolbar)
.allowBack()
if (isFirstCreation()) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
index b6481c9cbb..28849204c4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
@@ -42,7 +42,7 @@ import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.databinding.FragmentRoomListBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
@@ -104,8 +104,8 @@ class RoomListFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
analyticsScreenName = when (roomListParams.displayMode) {
- RoomListDisplayMode.PEOPLE -> Screen.ScreenName.MobilePeople
- RoomListDisplayMode.ROOMS -> Screen.ScreenName.MobileRooms
+ RoomListDisplayMode.PEOPLE -> MobileScreen.ScreenName.People
+ RoomListDisplayMode.ROOMS -> MobileScreen.ScreenName.Rooms
else -> null
}
}
@@ -121,7 +121,7 @@ class RoomListFragment @Inject constructor(
when (it) {
is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable)
- is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
+ is RoomListViewEvents.SelectRoom -> handleSelectRoom(it, it.isInviteAlreadyAccepted)
is RoomListViewEvents.Done -> Unit
is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
}.exhaustive
@@ -184,8 +184,8 @@ class RoomListFragment @Inject constructor(
super.onDestroyView()
}
- private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) {
- navigator.openRoom(requireActivity(), event.roomSummary.roomId)
+ private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom, isInviteAlreadyAccepted: Boolean) {
+ navigator.openRoom(context = requireActivity(), roomId = event.roomSummary.roomId, isInviteAlreadyAccepted = isInviteAlreadyAccepted)
}
private fun setupCreateRoomButton() {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt
index df2ff58da6..15e16c464f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt
@@ -27,7 +27,7 @@ sealed class RoomListViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
data class Failure(val throwable: Throwable) : RoomListViewEvents()
- data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents()
+ data class SelectRoom(val roomSummary: RoomSummary, val isInviteAlreadyAccepted: Boolean = false) : RoomListViewEvents()
object Done : RoomListViewEvents()
data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
index a5977501d2..4a81a8b526 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
@@ -33,7 +33,6 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.analytics.AnalyticsTracker
-import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.settings.VectorPreferences
@@ -174,7 +173,7 @@ class RoomListViewModel @AssistedInject constructor(
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
- _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
+ _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary, false))
}
private fun handleToggleSection(roomSection: RoomsSection) {
@@ -208,6 +207,7 @@ class RoomListViewModel @AssistedInject constructor(
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
+ _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary, true))
// quick echo
setState {
@@ -221,18 +221,6 @@ class RoomListViewModel @AssistedInject constructor(
}
)
}
-
- viewModelScope.launch {
- try {
- session.joinRoom(roomId)
- analyticsTracker.capture(action.roomSummary.toAnalyticsJoinedRoom())
- // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
- // Instead, we wait for the room to be joined
- } catch (failure: Throwable) {
- // Notify the user
- _viewEvents.post(RoomListViewEvents.Failure(failure))
- }
- }
}
private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->
diff --git a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt
index ff2e2a9cdb..ae285b074c 100644
--- a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt
+++ b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt
@@ -19,6 +19,7 @@
package im.vector.app.features.html
import android.content.Context
+import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
@@ -32,6 +33,7 @@ import im.vector.app.R
import im.vector.app.core.glide.GlideRequests
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.ref.WeakReference
@@ -117,6 +119,11 @@ class PillImageSpan(private val glideRequests: GlideRequests,
setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = icon
+ if (matrixItem is MatrixItem.EveryoneInRoomItem) {
+ chipBackgroundColor = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.colorError))
+ // setTextColor API does not exist right now for ChipDrawable, use textAppearance
+ setTextAppearanceResource(R.style.TextAppearance_Vector_Body_OnError)
+ }
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt
index f8a2ee5137..506f5e773c 100644
--- a/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt
+++ b/vector/src/main/java/im/vector/app/features/html/PillsPostProcessor.kt
@@ -36,57 +36,87 @@ class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomI
private val context: Context,
private val avatarRenderer: AvatarRenderer,
private val sessionHolder: ActiveSessionHolder) :
- EventHtmlRenderer.PostProcessor {
+ EventHtmlRenderer.PostProcessor {
+
+ /* ==========================================================================================
+ * Public api
+ * ========================================================================================== */
@AssistedFactory
interface Factory {
fun create(roomId: String?): PillsPostProcessor
}
+ /* ==========================================================================================
+ * Specialization
+ * ========================================================================================== */
+
override fun afterRender(renderedText: Spannable) {
addPillSpans(renderedText, roomId)
}
+ /* ==========================================================================================
+ * Helper methods
+ * ========================================================================================== */
+
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
+ addLinkSpans(renderedText, roomId)
+ }
+
+ private fun addPillSpan(
+ renderedText: Spannable,
+ pillSpan: PillImageSpan,
+ startSpan: Int,
+ endSpan: Int
+ ) {
+ renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+
+ private fun addLinkSpans(renderedText: Spannable, roomId: String?) {
// We let markdown handle links and then we add PillImageSpan if needed.
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
linkSpans.forEach { linkSpan ->
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(linkSpan)
- renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ addPillSpan(renderedText, pillSpan, startSpan, endSpan)
}
}
+ private fun createPillImageSpan(matrixItem: MatrixItem) =
+ PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
+
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
- val permalinkData = PermalinkParser.parse(url)
- val matrixItem = when (permalinkData) {
- is PermalinkData.UserLink -> {
- if (roomId == null) {
- sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem()
- } else {
- sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem()
- }
- }
- is PermalinkData.RoomLink -> {
- if (permalinkData.eventId == null) {
- val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias)
- if (permalinkData.isRoomAlias) {
- MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
- } else {
- MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
- }
- } else {
- // Exclude event link (used in reply events, we do not want to pill the "in reply to")
- null
- }
- }
- is PermalinkData.GroupLink -> {
- val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId)
- MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl)
- }
+ val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
+ is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId)
+ is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
+ is PermalinkData.GroupLink -> permalinkData.toMatrixItem()
else -> null
} ?: return null
- return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
+ return createPillImageSpan(matrixItem)
+ }
+
+ private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? =
+ if (roomId == null) {
+ sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()
+ } else {
+ sessionHolder.getSafeActiveSession()?.getRoomMember(userId, roomId)?.toMatrixItem()
+ }
+
+ private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
+ if (eventId == null) {
+ val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
+ when {
+ isRoomAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
+ else -> MatrixItem.RoomItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
+ }
+ } else {
+ // Exclude event link (used in reply events, we do not want to pill the "in reply to")
+ null
+ }
+
+ private fun PermalinkData.GroupLink.toMatrixItem(): MatrixItem? {
+ val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(groupId)
+ return MatrixItem.GroupItem(groupId, group?.displayName, group?.avatarUrl)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
index edc77d73f6..bf596fc6aa 100644
--- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt
@@ -38,7 +38,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityLoginBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.login.terms.LoginTermsFragment
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
@@ -81,7 +81,7 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA
override fun getCoordinatorLayout() = views.coordinatorLayout
override fun initUiAndData() {
- analyticsScreenName = Screen.ScreenName.Login
+ analyticsScreenName = MobileScreen.ScreenName.Login
if (isFirstCreation()) {
addFirstFragment()
@@ -203,7 +203,7 @@ open class LoginActivity : VectorBaseActivity(), UnlockedA
if (loginViewState.isUserLogged()) {
if (loginViewState.signMode == SignMode.SignUp) {
// change the screen name
- analyticsScreenName = Screen.ScreenName.Register
+ analyticsScreenName = MobileScreen.ScreenName.Register
}
val intent = HomeActivity.newIntent(
this,
diff --git a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt
index 0328d09427..d121245532 100644
--- a/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/login/LoginResetPasswordFragment.kt
@@ -31,7 +31,7 @@ import im.vector.app.core.extensions.hidePassword
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginResetPasswordBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -48,7 +48,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment(), Matri
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.RoomDirectory
+ analyticsScreenName = MobileScreen.ScreenName.RoomDirectory
sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
if (isFirstCreation()) {
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt
index 339c819a65..e4c350b88e 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomActivity.kt
@@ -26,7 +26,7 @@ import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import kotlinx.coroutines.flow.launchIn
@@ -57,7 +57,7 @@ class CreateRoomActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.CreateRoom
+ analyticsScreenName = MobileScreen.ScreenName.CreateRoom
sharedActionViewModel = viewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
sharedActionViewModel
.stream()
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt
index 48610dda7b..cb71f93a0e 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt
@@ -29,7 +29,7 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.roomdirectory.RoomDirectoryAction
import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.RoomDirectoryServer
@@ -54,7 +54,7 @@ class RoomDirectoryPickerFragment @Inject constructor(private val roomDirectoryP
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.MobileSwitchDirectory
+ analyticsScreenName = MobileScreen.ScreenName.SwitchDirectory
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
index c68bfca973..fcebe9adbb 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
@@ -47,7 +47,7 @@ import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.DialogShareQrCodeBinding
import im.vector.app.databinding.FragmentMatrixProfileBinding
import im.vector.app.databinding.ViewStubRoomMemberProfileHeaderBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
@@ -91,7 +91,7 @@ class RoomMemberProfileFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.User
+ analyticsScreenName = MobileScreen.ScreenName.User
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
index 8acf53088d..251b99e318 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
@@ -44,7 +44,7 @@ import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentMatrixProfileBinding
import im.vector.app.databinding.ViewStubRoomProfileHeaderBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailPendingAction
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
@@ -89,7 +89,7 @@ class RoomProfileFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.RoomSettings
+ analyticsScreenName = MobileScreen.ScreenName.RoomSettings
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
roomDetailPendingActionStore.data = RoomDetailPendingAction.OpenRoom(replacementRoomId, closeCurrentRoom = true)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt
index 3c1a763072..a0adf42d5b 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt
@@ -34,7 +34,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.saveMedia
import im.vector.app.core.utils.shareMedia
import im.vector.app.databinding.FragmentRoomUploadsBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.roomprofile.RoomProfileArgs
@@ -57,7 +57,7 @@ class RoomUploadsFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.RoomUploads
+ analyticsScreenName = MobileScreen.ScreenName.RoomUploads
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
index 7cefd20269..4185fde663 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt
@@ -30,7 +30,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast
import im.vector.app.features.analytics.AnalyticsTracker
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.screen.ScreenEvent
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -43,7 +43,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
* Analytics
* ========================================================================================== */
- protected var analyticsScreenName: Screen.ScreenName? = null
+ protected var analyticsScreenName: MobileScreen.ScreenName? = null
private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt
index cd76efac58..51011e29a2 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsRootFragment.kt
@@ -19,7 +19,7 @@ package im.vector.app.features.settings
import android.os.Bundle
import im.vector.app.R
import im.vector.app.core.preference.VectorPreference
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import javax.inject.Inject
class VectorSettingsRootFragment @Inject constructor() : VectorSettingsBaseFragment() {
@@ -29,7 +29,7 @@ class VectorSettingsRootFragment @Inject constructor() : VectorSettingsBaseFragm
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.MobileSettings
+ analyticsScreenName = MobileScreen.ScreenName.Settings
}
override fun bindPref() {
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
index e4e287e83a..ef87d908ea 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
@@ -51,7 +51,7 @@ import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.openFileSelection
import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogImportE2eKeysBinding
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewActions
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel
import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewState
@@ -94,7 +94,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- analyticsScreenName = Screen.ScreenName.MobileSettingsSecurity
+ analyticsScreenName = MobileScreen.ScreenName.SettingsSecurity
}
// cryptography
diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt
index 867526c009..631c375e62 100644
--- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt
@@ -31,7 +31,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDeactivateAccountBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
-import im.vector.app.features.analytics.plan.Screen
+import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.settings.VectorSettingsActivity
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
@@ -66,7 +66,7 @@ class DeactivateAccountFragment @Inject constructor() : VectorBaseFragment
-
-
\ No newline at end of file
+
+
diff --git a/vector/src/main/res/layout/fragment_space_create_choose_private_model.xml b/vector/src/main/res/layout/fragment_space_create_choose_private_model.xml
index e1e555eace..e3e5fe7e45 100644
--- a/vector/src/main/res/layout/fragment_space_create_choose_private_model.xml
+++ b/vector/src/main/res/layout/fragment_space_create_choose_private_model.xml
@@ -13,7 +13,7 @@
+
+
\ No newline at end of file
diff --git a/vector/src/main/res/layout/fragment_space_create_choose_type.xml b/vector/src/main/res/layout/fragment_space_create_choose_type.xml
index b675ec9b30..dd448bc967 100644
--- a/vector/src/main/res/layout/fragment_space_create_choose_type.xml
+++ b/vector/src/main/res/layout/fragment_space_create_choose_type.xml
@@ -12,26 +12,14 @@
-
-
+ app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toBottomOf="@id/headerText" />
+
+
+
+
+
diff --git a/vector/src/main/res/menu/home.xml b/vector/src/main/res/menu/home.xml
index 93947a5a7f..f15c31b4e9 100644
--- a/vector/src/main/res/menu/home.xml
+++ b/vector/src/main/res/menu/home.xml
@@ -31,9 +31,9 @@
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/values/config.xml b/vector/src/main/res/values/config.xml
index 8f23c2e1be..78b92cbfa4 100755
--- a/vector/src/main/res/values/config.xml
+++ b/vector/src/main/res/values/config.xml
@@ -5,7 +5,8 @@
https://matrix.org
- https://piwik.riot.im
+
+
https://riot.im/bugreports/submit
riot-android
element-auto-uisi
@@ -21,7 +22,7 @@
im.vector.app.android
- jitsi.riot.im
+ meet.element.io
- matrix.org
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index c155b6bb75..edf9ef189b 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -81,6 +81,10 @@
%s changed the server ACLs for this room.
You changed the server ACLs for this room.
+
+ - %d server ACLs change
+ - %d server ACLs changes
+
• Servers matching %s are now banned.
• Servers matching %s were removed from the ban list.
• Servers matching %s are now allowed.
@@ -3520,12 +3524,13 @@
Add Space
Your public space
Your private space
+
Spaces are a new way to group rooms and people
What type of space do you want to create?
You can change this later
To join an existing space, you need an invite.
Who are you working with?
- Make sure the right people have access to %s. You can change this later.
+ Make sure the right people have access to %s.
Just me
A private space to organise your rooms
Me and teammates
@@ -3782,4 +3787,7 @@
Show less
"%1$d more"
+ Notify the whole room
+ Users
+ Room notification
diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt
new file mode 100644
index 0000000000..b17c1a8bba
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/analytics/impl/DefaultVectorAnalyticsTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2022 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.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.FakeLateInitUserPropertiesFactory
+import im.vector.app.test.fakes.FakePostHog
+import im.vector.app.test.fakes.FakePostHogFactory
+import im.vector.app.test.fixtures.AnalyticsConfigFixture.anAnalyticsConfig
+import im.vector.app.test.fixtures.aUserProperties
+import im.vector.app.test.fixtures.aVectorAnalyticsEvent
+import im.vector.app.test.fixtures.aVectorAnalyticsScreen
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+
+private const val AN_ANALYTICS_ID = "analytics-id"
+private val A_SCREEN_EVENT = aVectorAnalyticsScreen()
+private val AN_EVENT = aVectorAnalyticsEvent()
+private val A_LATE_INIT_USER_PROPERTIES = aUserProperties()
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class DefaultVectorAnalyticsTest {
+
+ private val fakePostHog = FakePostHog()
+ private val fakeAnalyticsStore = FakeAnalyticsStore()
+ private val fakeLateInitUserPropertiesFactory = FakeLateInitUserPropertiesFactory()
+
+ private val defaultVectorAnalytics = DefaultVectorAnalytics(
+ postHogFactory = FakePostHogFactory(fakePostHog.instance).instance,
+ analyticsStore = fakeAnalyticsStore.instance,
+ globalScope = CoroutineScope(Dispatchers.Unconfined),
+ analyticsConfig = anAnalyticsConfig(isEnabled = true),
+ lateInitUserPropertiesFactory = fakeLateInitUserPropertiesFactory.instance
+ )
+
+ @Before
+ fun setUp() {
+ defaultVectorAnalytics.init()
+ }
+
+ @Test
+ fun `when setting user consent then updates analytics store`() = runBlockingTest {
+ defaultVectorAnalytics.setUserConsent(true)
+
+ fakeAnalyticsStore.verifyConsentUpdated(updatedValue = true)
+ }
+
+ @Test
+ fun `when consenting to analytics then updates posthog opt out to false`() = runBlockingTest {
+ fakeAnalyticsStore.givenUserContent(consent = true)
+
+ fakePostHog.verifyOptOutStatus(optedOut = false)
+ }
+
+ @Test
+ fun `when revoking consent to analytics then updates posthog opt out to true`() = runBlockingTest {
+ fakeAnalyticsStore.givenUserContent(consent = false)
+
+ fakePostHog.verifyOptOutStatus(optedOut = true)
+ }
+
+ @Test
+ fun `when setting the analytics id then updates analytics store`() = runBlockingTest {
+ defaultVectorAnalytics.setAnalyticsId(AN_ANALYTICS_ID)
+
+ fakeAnalyticsStore.verifyAnalyticsIdUpdated(updatedValue = AN_ANALYTICS_ID)
+ }
+
+ @Test
+ fun `given lateinit user properties when valid analytics id updates then identify with lateinit properties`() = runBlockingTest {
+ fakeLateInitUserPropertiesFactory.givenCreatesProperties(A_LATE_INIT_USER_PROPERTIES)
+
+ fakeAnalyticsStore.givenAnalyticsId(AN_ANALYTICS_ID)
+
+ fakePostHog.verifyIdentifies(AN_ANALYTICS_ID, A_LATE_INIT_USER_PROPERTIES)
+ }
+
+ @Test
+ fun `when signing out then resets posthog`() = runBlockingTest {
+ fakeAnalyticsStore.allowSettingAnalyticsIdToCallBackingFlow()
+
+ defaultVectorAnalytics.onSignOut()
+
+ fakePostHog.verifyReset()
+ }
+
+ @Test
+ fun `given user consent when tracking screen events then submits to posthog`() = runBlockingTest {
+ fakeAnalyticsStore.givenUserContent(consent = true)
+
+ defaultVectorAnalytics.screen(A_SCREEN_EVENT)
+
+ fakePostHog.verifyScreenTracked(A_SCREEN_EVENT.getName(), A_SCREEN_EVENT.toPostHogProperties())
+ }
+
+ @Test
+ fun `given user has not consented when tracking screen events then does not track`() = runBlockingTest {
+ fakeAnalyticsStore.givenUserContent(consent = false)
+
+ defaultVectorAnalytics.screen(A_SCREEN_EVENT)
+
+ fakePostHog.verifyNoScreenTracking()
+ }
+
+ @Test
+ fun `given user consent when tracking events then submits to posthog`() = runBlockingTest {
+ fakeAnalyticsStore.givenUserContent(consent = true)
+
+ defaultVectorAnalytics.capture(AN_EVENT)
+
+ fakePostHog.verifyEventTracked(AN_EVENT.getName(), AN_EVENT.toPostHogProperties())
+ }
+
+ @Test
+ fun `given user has not consented when tracking events then does not track`() = runBlockingTest {
+ fakeAnalyticsStore.givenUserContent(consent = false)
+
+ defaultVectorAnalytics.capture(AN_EVENT)
+
+ fakePostHog.verifyNoEventTracking()
+ }
+}
+
+private fun VectorAnalyticsScreen.toPostHogProperties(): Properties? {
+ return getProperties()?.let { properties ->
+ Properties().also { it.putAll(properties) }
+ }
+}
+
+private fun VectorAnalyticsEvent.toPostHogProperties(): Properties? {
+ return getProperties()?.let { properties ->
+ Properties().also { it.putAll(properties) }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactoryTest.kt b/vector/src/test/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactoryTest.kt
new file mode 100644
index 0000000000..c2fa50f789
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/analytics/impl/LateInitUserPropertiesFactoryTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022 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.impl
+
+import im.vector.app.features.analytics.plan.UserProperties
+import im.vector.app.features.onboarding.FtueUseCase
+import im.vector.app.test.fakes.FakeActiveSessionDataSource
+import im.vector.app.test.fakes.FakeContext
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.fakes.FakeVectorStore
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runBlockingTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class LateInitUserPropertiesFactoryTest {
+
+ private val fakeActiveSessionDataSource = FakeActiveSessionDataSource()
+ private val fakeVectorStore = FakeVectorStore()
+ private val fakeContext = FakeContext()
+ private val fakeSession = FakeSession().also {
+ it.givenVectorStore(fakeVectorStore.instance)
+ }
+
+ private val lateInitUserProperties = LateInitUserPropertiesFactory(
+ fakeActiveSessionDataSource.instance,
+ fakeContext.instance
+ )
+
+ @Test
+ fun `given no active session when creating properties then returns null`() = runBlockingTest {
+ val result = lateInitUserProperties.createUserProperties()
+
+ result shouldBeEqualTo null
+ }
+
+ @Test
+ fun `given no use case set on an active session when creating properties then returns null`() = runBlockingTest {
+ fakeVectorStore.givenUseCase(null)
+ fakeSession.givenVectorStore(fakeVectorStore.instance)
+ fakeActiveSessionDataSource.setActiveSession(fakeSession)
+
+ val result = lateInitUserProperties.createUserProperties()
+
+ result shouldBeEqualTo null
+ }
+
+ @Test
+ fun `given use case set on an active session when creating properties then includes the use case`() = runBlockingTest {
+ fakeVectorStore.givenUseCase(FtueUseCase.TEAMS)
+ fakeActiveSessionDataSource.setActiveSession(fakeSession)
+ val result = lateInitUserProperties.createUserProperties()
+
+ result shouldBeEqualTo UserProperties(
+ ftueUseCaseSelection = UserProperties.FtueUseCaseSelection.WorkMessaging
+ )
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionDataSource.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionDataSource.kt
new file mode 100644
index 0000000000..4dab6daf3b
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionDataSource.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 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.test.fakes
+
+import arrow.core.Option
+import im.vector.app.ActiveSessionDataSource
+import org.matrix.android.sdk.api.session.Session
+
+class FakeActiveSessionDataSource {
+
+ val instance = ActiveSessionDataSource()
+
+ fun setActiveSession(session: Session) {
+ instance.post(Option.just(session))
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsStore.kt
new file mode 100644
index 0000000000..6304da8d37
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAnalyticsStore.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022 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.test.fakes
+
+import im.vector.app.features.analytics.store.AnalyticsStore
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.runBlocking
+
+class FakeAnalyticsStore {
+
+ private val _consentFlow = MutableSharedFlow()
+ private val _idFlow = MutableSharedFlow()
+
+ val instance = mockk(relaxed = true) {
+ every { userConsentFlow } returns _consentFlow
+ every { analyticsIdFlow } returns _idFlow
+ }
+
+ fun allowSettingAnalyticsIdToCallBackingFlow() {
+ coEvery { instance.setAnalyticsId(any()) } answers {
+ runBlocking { _idFlow.emit(firstArg()) }
+ }
+ }
+
+ fun verifyConsentUpdated(updatedValue: Boolean) {
+ coVerify { instance.setUserConsent(updatedValue) }
+ }
+
+ suspend fun givenUserContent(consent: Boolean) {
+ _consentFlow.emit(consent)
+ }
+
+ fun verifyAnalyticsIdUpdated(updatedValue: String) {
+ coVerify { instance.setAnalyticsId(updatedValue) }
+ }
+
+ suspend fun givenAnalyticsId(id: String) {
+ _idFlow.emit(id)
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLateInitUserPropertiesFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLateInitUserPropertiesFactory.kt
new file mode 100644
index 0000000000..9b442ece73
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLateInitUserPropertiesFactory.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 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.test.fakes
+
+import im.vector.app.features.analytics.impl.LateInitUserPropertiesFactory
+import im.vector.app.features.analytics.plan.UserProperties
+import io.mockk.coEvery
+import io.mockk.mockk
+
+class FakeLateInitUserPropertiesFactory {
+
+ val instance = mockk()
+
+ fun givenCreatesProperties(userProperties: UserProperties?) {
+ coEvery { instance.createUserProperties() } returns userProperties
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt
new file mode 100644
index 0000000000..e14f809e66
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHog.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2022 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.test.fakes
+
+import android.os.Looper
+import com.posthog.android.PostHog
+import com.posthog.android.Properties
+import im.vector.app.features.analytics.plan.UserProperties
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.verify
+
+class FakePostHog {
+
+ init {
+ // workaround to avoid PostHog.HANDLER failing
+ mockkStatic(Looper::class)
+ val looper = mockk {
+ every { thread } returns Thread.currentThread()
+ }
+ every { Looper.getMainLooper() } returns looper
+ }
+
+ val instance = mockk(relaxed = true)
+
+ fun verifyOptOutStatus(optedOut: Boolean) {
+ verify { instance.optOut(optedOut) }
+ }
+
+ fun verifyIdentifies(analyticsId: String, userProperties: UserProperties?) {
+ verify {
+ val postHogProperties = userProperties?.getProperties()
+ ?.let { rawProperties -> Properties().also { it.putAll(rawProperties) } }
+ ?.takeIf { it.isNotEmpty() }
+ instance.identify(analyticsId, postHogProperties, null)
+ }
+ }
+
+ fun verifyReset() {
+ verify { instance.reset() }
+ }
+
+ fun verifyScreenTracked(name: String, properties: Properties?) {
+ verify { instance.screen(name, properties) }
+ }
+
+ fun verifyNoScreenTracking() {
+ verify(exactly = 0) {
+ instance.screen(any())
+ instance.screen(any(), any())
+ instance.screen(any(), any(), any())
+ }
+ }
+
+ fun verifyEventTracked(name: String, properties: Properties?) {
+ verify { instance.capture(name, properties) }
+ }
+
+ fun verifyNoEventTracking() {
+ verify(exactly = 0) {
+ instance.capture(any())
+ instance.capture(any(), any())
+ instance.capture(any(), any(), any())
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt
new file mode 100644
index 0000000000..1d18c97d32
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakePostHogFactory.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 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.test.fakes
+
+import com.posthog.android.PostHog
+import im.vector.app.features.analytics.impl.PostHogFactory
+import io.mockk.every
+import io.mockk.mockk
+
+class FakePostHogFactory(postHog: PostHog) {
+ val instance = mockk().also {
+ every { it.createPosthog() } returns postHog
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
index 91403b3b2c..a23c43b986 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
@@ -16,8 +16,12 @@
package im.vector.app.test.fakes
+import im.vector.app.core.extensions.vectorStore
+import im.vector.app.features.session.VectorSessionStore
import im.vector.app.test.testCoroutineDispatchers
+import io.mockk.coEvery
import io.mockk.mockk
+import io.mockk.mockkStatic
import org.matrix.android.sdk.api.session.Session
class FakeSession(
@@ -25,7 +29,19 @@ class FakeSession(
val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService()
) : Session by mockk(relaxed = true) {
+ init {
+ mockkStatic("im.vector.app.core.extensions.SessionKt")
+ }
+
override fun cryptoService() = fakeCryptoService
override val sharedSecretStorageService = fakeSharedSecretStorageService
override val coroutineDispatchers = testCoroutineDispatchers
+
+ fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
+ coEvery {
+ this@FakeSession.vectorStore(any())
+ } coAnswers {
+ vectorSessionStore
+ }
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorStore.kt
new file mode 100644
index 0000000000..22a4a5f6cf
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorStore.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022 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.test.fakes
+
+import im.vector.app.features.onboarding.FtueUseCase
+import im.vector.app.features.session.VectorSessionStore
+import io.mockk.coEvery
+import io.mockk.mockk
+
+class FakeVectorStore {
+ val instance = mockk()
+
+ fun givenUseCase(useCase: FtueUseCase?) {
+ coEvery {
+ instance.readUseCase()
+ } coAnswers {
+ useCase
+ }
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt
new file mode 100644
index 0000000000..5fbcdd98d1
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fixtures/AnalyticsConfigFixture.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2022 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.test.fixtures
+
+import im.vector.app.features.analytics.AnalyticsConfig
+
+object AnalyticsConfigFixture {
+ fun anAnalyticsConfig(
+ isEnabled: Boolean = false,
+ postHogHost: String = "http://posthog.url",
+ postHogApiKey: String = "api-key",
+ policyLink: String = "http://policy.link"
+ ) = object : AnalyticsConfig {
+ override val isEnabled: Boolean = isEnabled
+ override val postHogHost = postHogHost
+ override val postHogApiKey = postHogApiKey
+ override val policyLink = policyLink
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/UserPropertiesFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/UserPropertiesFixture.kt
new file mode 100644
index 0000000000..5a911e2bc9
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fixtures/UserPropertiesFixture.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 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.test.fixtures
+
+import im.vector.app.features.analytics.plan.UserProperties
+import im.vector.app.features.analytics.plan.UserProperties.FtueUseCaseSelection
+
+fun aUserProperties(
+ ftueUseCase: FtueUseCaseSelection? = FtueUseCaseSelection.Skip
+) = UserProperties(
+ ftueUseCaseSelection = ftueUseCase
+)
diff --git a/vector/src/test/java/im/vector/app/test/fixtures/VectorAnalyticsFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/VectorAnalyticsFixture.kt
new file mode 100644
index 0000000000..95590b0a44
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fixtures/VectorAnalyticsFixture.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.test.fixtures
+
+import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
+import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
+
+fun aVectorAnalyticsScreen(
+ name: String = "a-screen-name",
+ properties: Map? = null
+) = object : VectorAnalyticsScreen {
+ override fun getName() = name
+ override fun getProperties() = properties
+}
+
+fun aVectorAnalyticsEvent(
+ name: String = "an-event-name",
+ properties: Map? = null
+) = object : VectorAnalyticsEvent {
+ override fun getName() = name
+ override fun getProperties() = properties
+}