Merge branch 'develop' into feature/aris/thread_live_thread_list
# Conflicts: # matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
This commit is contained in:
commit
79a231f1dc
3
.github/workflows/sanity_test.yml
vendored
3
.github/workflows/sanity_test.yml
vendored
@ -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: |
|
||||
|
1
.idea/dictionaries/bmarty.xml
generated
1
.idea/dictionaries/bmarty.xml
generated
@ -28,6 +28,7 @@
|
||||
<w>previewable</w>
|
||||
<w>previewables</w>
|
||||
<w>pstn</w>
|
||||
<w>rageshake</w>
|
||||
<w>riotx</w>
|
||||
<w>signin</w>
|
||||
<w>signout</w>
|
||||
|
BIN
.idea/icon.png
generated
Normal file
BIN
.idea/icon.png
generated
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
1
changelog.d/2782.misc
Normal file
1
changelog.d/2782.misc
Normal file
@ -0,0 +1 @@
|
||||
Collapse successive ACLs events in room timeline
|
1
changelog.d/3771.feature
Normal file
1
changelog.d/3771.feature
Normal file
@ -0,0 +1 @@
|
||||
Open the room when user accepts an invite from the room list
|
1
changelog.d/4643.misc
Normal file
1
changelog.d/4643.misc
Normal file
@ -0,0 +1 @@
|
||||
Home screen: Replacing search icon by filter icon in the top right menu
|
1
changelog.d/5104.misc
Normal file
1
changelog.d/5104.misc
Normal file
@ -0,0 +1 @@
|
||||
Make Space creation screens more consistent
|
1
changelog.d/5123.feature
Normal file
1
changelog.d/5123.feature
Normal file
@ -0,0 +1 @@
|
||||
Add completion for @room to notify everyone in a room
|
1
changelog.d/5136.misc
Normal file
1
changelog.d/5136.misc
Normal file
@ -0,0 +1 @@
|
||||
Defensive coding to ensure encryption when room was once e2e
|
1
changelog.d/5185.sdk
Normal file
1
changelog.d/5185.sdk
Normal file
@ -0,0 +1 @@
|
||||
Deprecates Matrix.initialize and Matrix.getInstance in favour of the client providing its own singleton instance via Matrix.createInstance
|
1
changelog.d/5201.bugfix
Normal file
1
changelog.d/5201.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fix for call transfer with consult failing to make outgoing consultation call.
|
1
changelog.d/5225.misc
Normal file
1
changelog.d/5225.misc
Normal file
@ -0,0 +1 @@
|
||||
Replacing color "vctr_unread_room_badge" by "vctr_content_secondary"
|
1
changelog.d/5234.bugfix
Normal file
1
changelog.d/5234.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Analytics: Fixes missing use case identity values from within the onboarding flow
|
1
changelog.d/5243.bugfix
Normal file
1
changelog.d/5243.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Increments database schema to take advantage of homeserver capabilities entity migration (fixes crash in pre-release builds)
|
1
changelog.d/5254.misc
Normal file
1
changelog.d/5254.misc
Normal file
@ -0,0 +1 @@
|
||||
Change preferred jitsi domain from `jitsi.riot.im` to `meet.element.io`
|
1
changelog.d/5290.feature
Normal file
1
changelog.d/5290.feature
Normal file
@ -0,0 +1 @@
|
||||
Support creating disclosed polls
|
@ -57,11 +57,6 @@
|
||||
<attr name="vctr_list_separator_on_surface" format="color" />
|
||||
|
||||
<!-- Other colors, which are not in the palette -->
|
||||
<attr name="vctr_unread_room_badge" format="color" />
|
||||
<color name="vctr_unread_room_badge_light">@color/palette_gray_200</color>
|
||||
<color name="vctr_unread_room_badge_dark">@color/palette_gray_250</color>
|
||||
<color name="vctr_unread_room_badge_black">@color/palette_gray_250</color>
|
||||
|
||||
<attr name="vctr_fab_label_bg" format="color" />
|
||||
<color name="vctr_fab_label_bg_light">@android:color/white</color>
|
||||
<color name="vctr_fab_label_bg_dark">#FF181B21</color>
|
||||
|
@ -59,6 +59,10 @@
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Vector.Body.OnError">
|
||||
<item name="android:textColor">?colorOnError</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
|
||||
<item name="fontFamily">sans-serif</item>
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
@ -81,4 +85,4 @@
|
||||
<item name="android:letterSpacing">0.02</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -7,7 +7,6 @@
|
||||
<!-- Only setting the items we need to override to get the background to be pure black, otherwise inheriting -->
|
||||
|
||||
<!-- other colors -->
|
||||
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_black</item>
|
||||
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_black</item>
|
||||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_black</item>
|
||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>
|
||||
|
@ -16,7 +16,6 @@
|
||||
<item name="vctr_system">@color/element_system_dark</item>
|
||||
|
||||
<!-- other colors -->
|
||||
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_dark</item>
|
||||
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_dark</item>
|
||||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_dark</item>
|
||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>
|
||||
|
@ -16,7 +16,6 @@
|
||||
<item name="vctr_system">@color/element_system_light</item>
|
||||
|
||||
<!-- other colors -->
|
||||
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_light</item>
|
||||
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_light</item>
|
||||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_light</item>
|
||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -50,6 +50,9 @@ interface PushRuleService {
|
||||
|
||||
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
||||
|
||||
fun resolveSenderNotificationPermissionCondition(event: Event,
|
||||
condition: SenderNotificationPermissionCondition): Boolean
|
||||
|
||||
interface PushRuleListener {
|
||||
fun onEvents(pushEvents: PushEvents)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
||||
|
@ -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<Set<String>> {
|
||||
// Keywords are all content rules that don't start with '.'
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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")
|
||||
|
@ -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<Event>, 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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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<Unit> {
|
||||
|
@ -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<Unit> {
|
||||
@ -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<UIABaseAuth>) {
|
||||
promise.resume(
|
||||
UserPasswordAuth(
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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"
|
||||
|
@ -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<SpaceManageActivity> {
|
||||
waitUntilViewVisible(withId(R.id.roomList))
|
||||
}
|
||||
|
||||
onView(withId(R.id.roomList))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||
ViewMatchers.hasDescendant(withText(R.string.room_displayname_empty_room)),
|
||||
click()
|
||||
).atPosition(0)
|
||||
)
|
||||
clickOn(R.id.spaceAddRoomSaveItem)
|
||||
waitUntilActivityVisible<HomeActivity> {
|
||||
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))
|
||||
}
|
||||
}
|
@ -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<RecyclerView.ViewHolder>(
|
||||
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<InviteUsersToRoomActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.userListRecyclerView))
|
||||
}
|
||||
// close keyboard
|
||||
Espresso.pressBack()
|
||||
// close invite view
|
||||
Espresso.pressBack()
|
||||
}
|
||||
|
||||
fun spaceMembers() {
|
||||
clickOn(R.id.showMemberList)
|
||||
waitUntilActivityVisible<RoomProfileActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||
}
|
||||
Espresso.pressBack()
|
||||
}
|
||||
|
||||
fun spaceSettings(block: SpaceSettingsRobot.() -> Unit) {
|
||||
clickOn(R.id.spaceSettings)
|
||||
waitUntilActivityVisible<SpaceManageActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||
}
|
||||
block(SpaceSettingsRobot())
|
||||
}
|
||||
|
||||
fun exploreRooms() {
|
||||
clickOn(R.id.exploreRooms)
|
||||
waitUntilActivityVisible<SpaceExploreActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.spaceDirectoryList))
|
||||
}
|
||||
Espresso.pressBack()
|
||||
}
|
||||
|
||||
fun addRoom() = apply {
|
||||
clickOn(R.id.addRooms)
|
||||
waitUntilActivityVisible<SpaceManageActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||
}
|
||||
Espresso.pressBack()
|
||||
}
|
||||
|
||||
fun addSpace() = apply {
|
||||
clickOn(R.id.addSpaces)
|
||||
waitUntilActivityVisible<SpaceManageActivity> {
|
||||
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<SpaceLeaveAdvancedActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||
}
|
||||
clickOn(R.id.spaceLeaveButton)
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<RecyclerView.ViewHolder>(
|
||||
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.room_settings_space_access_title)),
|
||||
ViewActions.click()
|
||||
)
|
||||
)
|
||||
|
||||
waitUntilActivityVisible<RoomJoinRuleActivity> {
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.genericRecyclerView))
|
||||
}
|
||||
|
||||
Espresso.pressBack()
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||
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<RecyclerView.ViewHolder>(
|
||||
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.space_settings_permissions_title)),
|
||||
ViewActions.click()
|
||||
)
|
||||
)
|
||||
|
||||
waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||
Espresso.pressBack()
|
||||
Espresso.pressBack()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<VB : ViewBinding> : 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
|
||||
|
@ -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<VB : ViewBinding> : BottomShe
|
||||
* Analytics
|
||||
* ========================================================================================== */
|
||||
|
||||
protected var analyticsScreenName: Screen.ScreenName? = null
|
||||
protected var analyticsScreenName: MobileScreen.ScreenName? = null
|
||||
private var screenEvent: ScreenEvent? = null
|
||||
|
||||
protected lateinit var analyticsTracker: AnalyticsTracker
|
||||
|
@ -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<VB : ViewBinding> : 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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<Boolean> {
|
||||
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
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
@ -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()
|
||||
)
|
||||
|
@ -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<AutocompleteHeaderItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute var title: String? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.titleView.text = title
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val titleView by bind<TextView>(R.id.headerItemAutocompleteTitle)
|
||||
}
|
||||
}
|
@ -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<List<RoomMemberSummary>>() {
|
||||
class AutocompleteMemberController @Inject constructor(private val context: Context) :
|
||||
TypedEpoxyController<List<AutocompleteMemberItem>>() {
|
||||
|
||||
var listener: AutocompleteClickListener<RoomMemberSummary>? = null
|
||||
/* ==========================================================================================
|
||||
* Fields
|
||||
* ========================================================================================== */
|
||||
|
||||
var listener: AutocompleteClickListener<AutocompleteMemberItem>? = null
|
||||
|
||||
/* ==========================================================================================
|
||||
* Dependencies
|
||||
* ========================================================================================== */
|
||||
|
||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
override fun buildModels(data: List<RoomMemberSummary>?) {
|
||||
/* ==========================================================================================
|
||||
* Specialization
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun buildModels(data: List<AutocompleteMemberItem>?) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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<RoomMemberSummary>(context), AutocompleteClickListener<RoomMemberSummary> {
|
||||
) : RecyclerViewPresenter<AutocompleteMemberItem>(context), AutocompleteClickListener<AutocompleteMemberItem> {
|
||||
|
||||
/* ==========================================================================================
|
||||
* 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<AutocompleteMemberItem>().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"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<ActivityCallBinding>(), CallContro
|
||||
?.let {
|
||||
callViewModel.handle(VectorCallViewActions.SwitchCall(it))
|
||||
}
|
||||
this.intent = intent
|
||||
}
|
||||
|
||||
override fun getMenuRes() = R.menu.vector_call
|
||||
@ -522,14 +524,21 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), 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)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(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<VectorCallViewModel, VectorCallViewState> {
|
||||
override fun create(initialState: VectorCallViewState): VectorCallViewModel
|
||||
|
@ -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() {
|
||||
|
@ -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<ActivityCallTransferBinding>() {
|
||||
|
||||
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<ActivityCallTransferBinding>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -20,6 +20,4 @@ import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
sealed class CallTransferViewEvents : VectorViewEvents {
|
||||
object Complete : CallTransferViewEvents()
|
||||
object Loading : CallTransferViewEvents()
|
||||
object FailToTransfer : CallTransferViewEvents()
|
||||
}
|
||||
|
@ -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<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
|
||||
VectorViewModel<CallTransferViewState, EmptyAction, CallTransferViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<CallTransferViewModel, CallTransferViewState> {
|
||||
@ -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) { }
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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<Command>(editText)
|
||||
.with(commandAutocompletePolicy)
|
||||
.with(autocompleteCommandPresenter)
|
||||
.with(ELEVATION)
|
||||
.with(ELEVATION_DP)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<Command> {
|
||||
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<RoomMemberSummary>(editText)
|
||||
.with(CharPolicy('@', true))
|
||||
Autocomplete.on<AutocompleteMemberItem>(editText)
|
||||
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_MEMBERS, true))
|
||||
.with(autocompleteMemberPresenter)
|
||||
.with(ELEVATION)
|
||||
.with(ELEVATION_DP)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<RoomMemberSummary> {
|
||||
override fun onPopupItemClicked(editable: Editable, item: RoomMemberSummary): Boolean {
|
||||
insertMatrixItem(editText, editable, "@", item.toMatrixItem())
|
||||
return true
|
||||
.with(object : AutocompleteCallback<AutocompleteMemberItem> {
|
||||
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<RoomSummary>(editText)
|
||||
.with(CharPolicy('#', true))
|
||||
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_ROOMS, true))
|
||||
.with(autocompleteRoomPresenter)
|
||||
.with(ELEVATION)
|
||||
.with(ELEVATION_DP)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<RoomSummary> {
|
||||
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<GroupSummary>(editText)
|
||||
.with(CharPolicy('+', true))
|
||||
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_GROUPS, true))
|
||||
.with(autocompleteGroupPresenter)
|
||||
.with(ELEVATION)
|
||||
.with(ELEVATION_DP)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<GroupSummary> {
|
||||
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<String>(editText)
|
||||
.with(CharPolicy(':', false))
|
||||
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_EMOJIS, false))
|
||||
.with(autocompleteEmojiPresenter)
|
||||
.with(ELEVATION)
|
||||
.with(ELEVATION_DP)
|
||||
.with(backgroundDrawable)
|
||||
.with(object : AutocompleteCallback<String> {
|
||||
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 = ':'
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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<String>) : RoomDetailViewEvents()
|
||||
|
||||
data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents()
|
||||
|
@ -49,6 +49,7 @@ data class JitsiState(
|
||||
data class RoomDetailViewState(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
val isInviteAlreadyAccepted: Boolean,
|
||||
val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
|
||||
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
||||
val asyncRoomSummary: Async<RoomSummary> = 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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<EventHtmlRenderer>,
|
||||
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(
|
||||
|
@ -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 {
|
||||
|
@ -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<MergedMembershipEventsItem.Holder>() {
|
||||
abstract class MergedSimilarEventsItem : BasedMergedItem<MergedSimilarEventsItem.Holder>() {
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
@ -37,7 +38,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
if (attributes.isCollapsed) {
|
||||
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size)
|
||||
val summary = holder.expandView.resources.getQuantityString(attributes.summaryTitleResId, attributes.mergeData.size, attributes.mergeData.size)
|
||||
holder.summaryView.text = summary
|
||||
holder.summaryView.visibility = View.VISIBLE
|
||||
holder.avatarListView.visibility = View.VISIBLE
|
||||
@ -66,6 +67,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
||||
}
|
||||
|
||||
data class Attributes(
|
||||
@PluralsRes val summaryTitleResId: Int,
|
||||
override val isCollapsed: Boolean,
|
||||
override val mergeData: List<Data>,
|
||||
override val avatarRenderer: AvatarRenderer,
|
@ -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)
|
||||
}
|
||||
}
|
@ -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<ActivityFilteredRoomsBinding>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
analyticsScreenName = Screen.ScreenName.RoomFilter
|
||||
analyticsScreenName = MobileScreen.ScreenName.RoomFilter
|
||||
setupToolbar(views.filteredRoomsToolbar)
|
||||
.allowBack()
|
||||
if (isFirstCreation()) {
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 ->
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), 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,
|
||||
|
@ -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<F
|
||||
private var showWarning = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
analyticsScreenName = Screen.ScreenName.ForgotPassword
|
||||
analyticsScreenName = MobileScreen.ScreenName.ForgotPassword
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.FragmentLoginSplashBinding
|
||||
import im.vector.app.features.analytics.plan.Screen
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import java.net.UnknownHostException
|
||||
@ -44,7 +44,7 @@ class LoginSplashFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
analyticsScreenName = Screen.ScreenName.Welcome
|
||||
analyticsScreenName = MobileScreen.ScreenName.Welcome
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user