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:
ariskotsomitopoulos 2022-02-22 12:52:55 +02:00
commit 79a231f1dc
136 changed files with 2146 additions and 598 deletions

View File

@ -69,9 +69,10 @@ jobs:
touch emulator.log touch emulator.log
chmod 777 emulator.log chmod 777 emulator.log
adb logcat >> 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 - name: Upload Test Report Log
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
if: always()
with: with:
name: sanity-error-results name: sanity-error-results
path: | path: |

View File

@ -28,6 +28,7 @@
<w>previewable</w> <w>previewable</w>
<w>previewables</w> <w>previewables</w>
<w>pstn</w> <w>pstn</w>
<w>rageshake</w>
<w>riotx</w> <w>riotx</w>
<w>signin</w> <w>signin</w>
<w>signout</w> <w>signout</w>

BIN
.idea/icon.png generated Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

1
changelog.d/2782.misc Normal file
View File

@ -0,0 +1 @@
Collapse successive ACLs events in room timeline

1
changelog.d/3771.feature Normal file
View File

@ -0,0 +1 @@
Open the room when user accepts an invite from the room list

1
changelog.d/4643.misc Normal file
View File

@ -0,0 +1 @@
Home screen: Replacing search icon by filter icon in the top right menu

1
changelog.d/5104.misc Normal file
View File

@ -0,0 +1 @@
Make Space creation screens more consistent

1
changelog.d/5123.feature Normal file
View File

@ -0,0 +1 @@
Add completion for @room to notify everyone in a room

1
changelog.d/5136.misc Normal file
View File

@ -0,0 +1 @@
Defensive coding to ensure encryption when room was once e2e

1
changelog.d/5185.sdk Normal file
View 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
View File

@ -0,0 +1 @@
Fix for call transfer with consult failing to make outgoing consultation call.

1
changelog.d/5225.misc Normal file
View File

@ -0,0 +1 @@
Replacing color "vctr_unread_room_badge" by "vctr_content_secondary"

1
changelog.d/5234.bugfix Normal file
View File

@ -0,0 +1 @@
Analytics: Fixes missing use case identity values from within the onboarding flow

1
changelog.d/5243.bugfix Normal file
View 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
View File

@ -0,0 +1 @@
Change preferred jitsi domain from `jitsi.riot.im` to `meet.element.io`

1
changelog.d/5290.feature Normal file
View File

@ -0,0 +1 @@
Support creating disclosed polls

View File

@ -57,11 +57,6 @@
<attr name="vctr_list_separator_on_surface" format="color" /> <attr name="vctr_list_separator_on_surface" format="color" />
<!-- Other colors, which are not in the palette --> <!-- 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" /> <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_light">@android:color/white</color>
<color name="vctr_fab_label_bg_dark">#FF181B21</color> <color name="vctr_fab_label_bg_dark">#FF181B21</color>

View File

@ -59,6 +59,10 @@
<item name="android:fontFamily">sans-serif-medium</item> <item name="android:fontFamily">sans-serif-medium</item>
</style> </style>
<style name="TextAppearance.Vector.Body.OnError">
<item name="android:textColor">?colorOnError</item>
</style>
<style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption"> <style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="android:fontFamily">sans-serif</item> <item name="android:fontFamily">sans-serif</item>

View File

@ -7,7 +7,6 @@
<!-- Only setting the items we need to override to get the background to be pure black, otherwise inheriting --> <!-- Only setting the items we need to override to get the background to be pure black, otherwise inheriting -->
<!-- other colors --> <!-- 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_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_stroke">@color/vctr_fab_label_stroke_black</item>
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item> <item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>

View File

@ -16,7 +16,6 @@
<item name="vctr_system">@color/element_system_dark</item> <item name="vctr_system">@color/element_system_dark</item>
<!-- other colors --> <!-- 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_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_stroke">@color/vctr_fab_label_stroke_dark</item>
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item> <item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>

View File

@ -16,7 +16,6 @@
<item name="vctr_system">@color/element_system_light</item> <item name="vctr_system">@color/element_system_light</item>
<!-- other colors --> <!-- 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_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_stroke">@color/vctr_fab_label_stroke_light</item>
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item> <item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>

View File

@ -99,12 +99,31 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
private lateinit var instance: Matrix private lateinit var instance: Matrix
private val isInit = AtomicBoolean(false) 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) { fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
if (isInit.compareAndSet(false, true)) { if (isInit.compareAndSet(false, true)) {
instance = Matrix(context.applicationContext, matrixConfiguration) 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 { fun getInstance(context: Context): Matrix {
if (isInit.compareAndSet(false, true)) { if (isInit.compareAndSet(false, true)) {
val appContext = context.applicationContext val appContext = context.applicationContext
@ -113,7 +132,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
instance = Matrix(appContext, matrixConfiguration) instance = Matrix(appContext, matrixConfiguration)
} else { } else {
throw IllegalStateException("Matrix is not initialized properly." + 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 return instance

View File

@ -66,6 +66,7 @@ data class MatrixConfiguration(
/** /**
* Can be implemented by your Application class. * Can be implemented by your Application class.
*/ */
@Deprecated("Use Matrix.createInstance and manage the instance manually instead of Matrix.getInstance")
interface Provider { interface Provider {
fun providesMatrixConfiguration(): MatrixConfiguration fun providesMatrixConfiguration(): MatrixConfiguration
} }

View File

@ -50,6 +50,9 @@ interface PushRuleService {
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? // fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
fun resolveSenderNotificationPermissionCondition(event: Event,
condition: SenderNotificationPermissionCondition): Boolean
interface PushRuleListener { interface PushRuleListener {
fun onEvents(pushEvents: PushEvents) fun onEvents(pushEvents: PushEvents)
} }

View File

@ -36,7 +36,19 @@ sealed class MatrixItem(
data class UserItem(override val id: String, data class UserItem(override val id: String,
override val displayName: String? = null, override val displayName: String? = null,
override val avatarUrl: 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 { init {
if (BuildConfig.DEBUG) checkId() 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) * Return the prefix as defined in the matrix spec (and not extracted from the id)
*/ */
fun getIdPrefix() = when (this) { private fun getIdPrefix() = when (this) {
is UserItem -> '@' is UserItem -> '@'
is EventItem -> '$' is EventItem -> '$'
is SpaceItem, is SpaceItem,
is RoomItem -> '!' is RoomItem,
is EveryoneInRoomItem -> '!'
is RoomAliasItem -> '#' is RoomAliasItem -> '#'
is GroupItem -> '+' is GroupItem -> '+'
} }
fun firstLetterOfDisplayName(): String { 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) return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn -> .let { dn ->
var startIndex = 0 var startIndex = 0
@ -152,7 +170,8 @@ sealed class MatrixItem(
} }
companion object { 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.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 // If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)

View File

@ -35,6 +35,8 @@ internal class CryptoSessionInfoProvider @Inject constructor(
) { ) {
fun isRoomEncrypted(roomId: String): Boolean { 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 -> val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.isEmpty(EventEntityFields.STATE_KEY) .isEmpty(EventEntityFields.STATE_KEY)

View File

@ -240,6 +240,14 @@ internal interface IMXCryptoStore {
*/ */
fun getRoomAlgorithm(roomId: String): String? 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 shouldEncryptForInvitedMembers(roomId: String): Boolean
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)

View File

@ -631,7 +631,15 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
doRealmTransaction(realmConfiguration) { 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 { override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
return doWithRealm(realmConfiguration) { return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers

View File

@ -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.MigrateCryptoTo012
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013 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.MigrateCryptoTo014
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -46,7 +47,7 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration
// 0, 1, 2: legacy Riot-Android // 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema // 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) // 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) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") 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 < 12) MigrateCryptoTo012(realm).perform()
if (oldVersion < 13) MigrateCryptoTo013(realm).perform() if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
if (oldVersion < 14) MigrateCryptoTo014(realm).perform() if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
} }
} }

View File

@ -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)
}
}
}

View File

@ -27,7 +27,10 @@ internal open class CryptoRoomEntity(
// Store the current outbound session for this room, // Store the current outbound session for this room,
// to avoid re-create and re-share at each startup (if rotation not needed..) // 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 // 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() { RealmObject() {

View File

@ -19,11 +19,13 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.pushrules.Action 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.PushEvents
import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.PushRuleService
import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleKind
import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.pushrules.RuleScope
import org.matrix.android.sdk.api.pushrules.RuleSetKey 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.getActions
import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.api.pushrules.rest.RuleSet import org.matrix.android.sdk.api.pushrules.rest.RuleSet
@ -53,6 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
private val removePushRuleTask: RemovePushRuleTask, private val removePushRuleTask: RemovePushRuleTask,
private val pushRuleFinder: PushRuleFinder, private val pushRuleFinder: PushRuleFinder,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val conditionResolver: ConditionResolver,
@SessionDatabase private val monarchy: Monarchy @SessionDatabase private val monarchy: Monarchy
) : PushRuleService { ) : PushRuleService {
@ -143,6 +146,10 @@ internal class DefaultPushRuleService @Inject constructor(
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty() 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>> { override fun getKeywords(): LiveData<Set<String>> {
// Keywords are all content rules that don't start with '.' // Keywords are all content rules that don't start with '.'
val liveData = monarchy.findAllMappedWithChanges( val liveData = monarchy.findAllMappedWithChanges(

View File

@ -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.NoOpCancellable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional 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.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
@ -47,7 +46,6 @@ internal class DefaultRelationService @AssistedInject constructor(
private val eventEditor: EventEditor, private val eventEditor: EventEditor,
private val eventSenderProcessor: EventSenderProcessor, private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
@ -144,7 +142,7 @@ internal class DefaultRelationService @AssistedInject constructor(
?.also { saveLocalEcho(it) } ?.also { saveLocalEcho(it) }
?: return null ?: return null
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) return eventSenderProcessor.postEvent(event)
} }
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
@ -200,7 +198,7 @@ internal class DefaultRelationService @AssistedInject constructor(
saveLocalEcho(it) saveLocalEcho(it)
} }
} }
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) return eventSenderProcessor.postEvent(event)
} }
/** /**

View File

@ -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.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable 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.database.mapper.toEntity
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository 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, internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val localEchoRepository: LocalEchoRepository) { private val localEchoRepository: LocalEchoRepository) {
fun editTextMessage(targetEvent: TimelineEvent, fun editTextMessage(targetEvent: TimelineEvent,
@ -51,7 +49,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
} else if (targetEvent.root.sendState.isSent()) { } else if (targetEvent.root.sendState.isSent()) {
val event = eventFactory val event = eventFactory
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) .createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
return sendReplaceEvent(roomId, event) return sendReplaceEvent(event)
} else { } else {
// Should we throw? // Should we throw?
Timber.w("Can't edit a sending event") 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()) { } else if (targetEvent.root.sendState.isSent()) {
val event = eventFactory val event = eventFactory
.createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options) .createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options)
return sendReplaceEvent(roomId, event) return sendReplaceEvent(event)
} else { } else {
Timber.w("Can't edit a sending event") Timber.w("Can't edit a sending event")
return NoOpCancellable return NoOpCancellable
@ -82,12 +80,12 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable { private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable {
val roomId = targetEvent.roomId val roomId = targetEvent.roomId
updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent) 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) localEchoRepository.createLocalEcho(editedEvent)
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) return eventSenderProcessor.postEvent(editedEvent)
} }
fun editReply(replyToEdit: TimelineEvent, fun editReply(replyToEdit: TimelineEvent,
@ -107,7 +105,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
eventId = replyToEdit.eventId eventId = replyToEdit.eventId
) ?: return NoOpCancellable ) ?: return NoOpCancellable
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent) updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) return eventSenderProcessor.postEvent(editedEvent)
} else if (replyToEdit.root.sendState.isSent()) { } else if (replyToEdit.root.sendState.isSent()) {
val event = eventFactory.createReplaceTextOfReply( val event = eventFactory.createReplaceTextOfReply(
roomId, roomId,
@ -119,7 +117,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
compatibilityBodyText compatibilityBodyText
) )
.also { localEchoRepository.createLocalEcho(it) } .also { localEchoRepository.createLocalEcho(it) }
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) return eventSenderProcessor.postEvent(event)
} else { } else {
// Should we throw? // Should we throw?
Timber.w("Can't edit a sending event") Timber.w("Can't edit a sending event")

View File

@ -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.CancelableBag
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable 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.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@ -66,7 +66,7 @@ internal class DefaultSendService @AssistedInject constructor(
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val cryptoStore: IMXCryptoStore,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val eventSenderProcessor: EventSenderProcessor, private val eventSenderProcessor: EventSenderProcessor,
@ -303,7 +303,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
val cancelableBag = CancelableBag() val cancelableBag = CancelableBag()
allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) } allLocalEchoes.groupBy { cryptoStore.roomWasOnceEncrypted(it.roomId!!) }
.apply { .apply {
keys.forEach { isRoomEncrypted -> keys.forEach { isRoomEncrypted ->
// Should never be empty // Should never be empty
@ -334,7 +334,7 @@ internal class DefaultSendService @AssistedInject constructor(
} }
private fun sendEvent(event: Event): Cancelable { private fun sendEvent(event: Event): Cancelable {
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!)) return eventSenderProcessor.postEvent(event)
} }
private fun createLocalEcho(event: Event) { private fun createLocalEcho(event: Event) {

View File

@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills
import android.text.SpannableString import android.text.SpannableString
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan 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 org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
import java.util.Collections import java.util.Collections
import javax.inject.Inject import javax.inject.Inject
@ -51,6 +52,8 @@ internal class TextPillsUtils @Inject constructor(
val pills = spannableString val pills = spannableString
?.getSpans(0, text.length, MatrixItemSpan::class.java) ?.getSpans(0, text.length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) } ?.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() ?.toMutableList()
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
?: return null ?: return null

View File

@ -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.getRetryDelay
import org.matrix.android.sdk.api.failure.isLimitExceededError import org.matrix.android.sdk.api.failure.isLimitExceededError
import org.matrix.android.sdk.api.session.Session 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.session.events.model.Event
import org.matrix.android.sdk.api.util.Cancelable 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.session.SessionScope
import org.matrix.android.sdk.internal.task.CoroutineSequencer import org.matrix.android.sdk.internal.task.CoroutineSequencer
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
@ -54,7 +54,7 @@ private const val MAX_RETRY_COUNT = 3
*/ */
@SessionScope @SessionScope
internal class EventSenderProcessorCoroutine @Inject constructor( internal class EventSenderProcessorCoroutine @Inject constructor(
private val cryptoService: CryptoService, private val cryptoStore: IMXCryptoStore,
private val sessionParams: SessionParams, private val sessionParams: SessionParams,
private val queuedTaskFactory: QueuedTaskFactory, private val queuedTaskFactory: QueuedTaskFactory,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
@ -92,7 +92,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
} }
override fun postEvent(event: Event): Cancelable { 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 { override fun postEvent(event: Event, encrypt: Boolean): Cancelable {

View File

@ -119,9 +119,8 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.roomType = roomType roomSummaryEntity.roomType = roomType
Timber.v("## Space: Updating summary room [$roomId] 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 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) val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)

View File

@ -192,12 +192,14 @@ abstract class SyncService : Service() {
} }
} }
abstract fun provideMatrix(): Matrix
private fun initialize(intent: Intent?): Boolean { private fun initialize(intent: Intent?): Boolean {
if (intent == null) { if (intent == null) {
Timber.d("## Sync: initialize intent is null") Timber.d("## Sync: initialize intent is null")
return false return false
} }
val matrix = Matrix.getInstance(applicationContext) val matrix = provideMatrix()
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds()) syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds())
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds()) syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds())

View File

@ -38,7 +38,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest 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.MainActivity
import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeActivity
@ -47,7 +47,6 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -61,8 +60,7 @@ class SecurityBootstrapTest : VerificationTestBase() {
@Before @Before
fun createSessionWithCrossSigning() { fun createSessionWithCrossSigning() {
val context = InstrumentationRegistry.getInstrumentation().targetContext val matrix = getMatrixInstance()
val matrix = Matrix.getInstance(context)
val userName = "foobar_${System.currentTimeMillis()}" val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
stubAllExternalIntents() stubAllExternalIntents()

View File

@ -33,7 +33,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest 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.MainActivity
import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeActivity
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
@ -41,7 +41,6 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
@ -66,8 +65,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
@Before @Before
fun createSessionWithCrossSigning() { fun createSessionWithCrossSigning() {
val context = InstrumentationRegistry.getInstrumentation().targetContext val matrix = getMatrixInstance()
val matrix = Matrix.getInstance(context)
val userName = "foobar_${System.currentTimeMillis()}" val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> { doSync<Unit> {

View File

@ -34,6 +34,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.core.resources.StringProvider 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.MainActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
@ -45,7 +46,6 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
@ -67,7 +67,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
@Before @Before
fun createSessionWithCrossSigningAnd4S() { fun createSessionWithCrossSigningAnd4S() {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
val matrix = Matrix.getInstance(context) val matrix = getMatrixInstance()
val userName = "foobar_${System.currentTimeMillis()}" val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> { doSync<Unit> {

View File

@ -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)
}

View File

@ -40,7 +40,7 @@ private val deviceLanguage = Locale.getDefault().language
class ScreenshotFailureRule : TestWatcher() { class ScreenshotFailureRule : TestWatcher() {
override fun failed(e: Throwable?, description: Description) { 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() val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
storeFailureScreenshot(bitmap, screenShotName) storeFailureScreenshot(bitmap, screenShotName)
} }

View File

@ -16,10 +16,12 @@
package im.vector.app.ui package im.vector.app.ui
import android.Manifest
import androidx.test.espresso.IdlingPolicies import androidx.test.espresso.IdlingPolicies
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import androidx.test.rule.GrantPermissionRule
import im.vector.app.R import im.vector.app.R
import im.vector.app.espresso.tools.ScreenshotFailureRule import im.vector.app.espresso.tools.ScreenshotFailureRule
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
@ -43,6 +45,7 @@ class UiAllScreensSanityTest {
@get:Rule @get:Rule
val testRule = RuleChain val testRule = RuleChain
.outerRule(ActivityScenarioRule(MainActivity::class.java)) .outerRule(ActivityScenarioRule(MainActivity::class.java))
.around(GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE))
.around(ScreenshotFailureRule()) .around(ScreenshotFailureRule())
private val elementRobot = ElementRobot() 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 { elementRobot.withDeveloperMode {
settings { settings {
advancedSettings { crawlDeveloperOptions() } advancedSettings { crawlDeveloperOptions() }

View File

@ -35,6 +35,7 @@ import im.vector.app.features.home.HomeActivity
import im.vector.app.features.onboarding.OnboardingActivity import im.vector.app.features.onboarding.OnboardingActivity
import im.vector.app.initialSyncIdlingResource import im.vector.app.initialSyncIdlingResource
import im.vector.app.ui.robot.settings.SettingsRobot import im.vector.app.ui.robot.settings.SettingsRobot
import im.vector.app.ui.robot.space.SpaceRobot
import im.vector.app.withIdlingResource import im.vector.app.withIdlingResource
import timber.log.Timber import timber.log.Timber
@ -147,6 +148,10 @@ class ElementRobot {
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer)) waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
}.onFailure { Timber.w(it, "Verification popup missing") } }.onFailure { Timber.w(it, "Verification popup missing") }
} }
fun space(block: SpaceRobot.() -> Unit) {
block(SpaceRobot())
}
} }
private fun Boolean.toWarningType() = if (this) "shown" else "skipped" private fun Boolean.toWarningType() = if (this) "shown" else "skipped"

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}

View File

@ -55,7 +55,6 @@ import im.vector.app.features.pin.PinLocker
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.rageshake.VectorFileLogger import im.vector.app.features.rageshake.VectorFileLogger
import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler 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.VectorLocale
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.themes.ThemeUtils 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 im.vector.app.push.fcm.FcmHelper
import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler
import org.matrix.android.sdk.api.Matrix 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.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import timber.log.Timber import timber.log.Timber
@ -77,7 +75,6 @@ import androidx.work.Configuration as WorkConfiguration
@HiltAndroidApp @HiltAndroidApp
class VectorApplication : class VectorApplication :
Application(), Application(),
MatrixConfiguration.Provider,
WorkConfiguration.Provider { WorkConfiguration.Provider {
lateinit var appContext: Context lateinit var appContext: Context
@ -100,6 +97,7 @@ class VectorApplication :
@Inject lateinit var autoRageShaker: AutoRageShaker @Inject lateinit var autoRageShaker: AutoRageShaker
@Inject lateinit var vectorFileLogger: VectorFileLogger @Inject lateinit var vectorFileLogger: VectorFileLogger
@Inject lateinit var vectorAnalytics: VectorAnalytics @Inject lateinit var vectorAnalytics: VectorAnalytics
@Inject lateinit var matrix: Matrix
// font thread handler // font thread handler
private var fontThreadHandler: Handler? = null 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 { override fun getWorkManagerConfiguration(): WorkConfiguration {
return WorkConfiguration.Builder() return WorkConfiguration.Builder()
.setWorkerFactory(Matrix.getInstance(this.appContext).workerFactory()) .setWorkerFactory(matrix.workerFactory())
.setExecutor(Executors.newCachedThreadPool()) .setExecutor(Executors.newCachedThreadPool())
.build() .build()
} }

View File

@ -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

View File

@ -26,13 +26,16 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import im.vector.app.BuildConfig
import im.vector.app.EmojiCompatWrapper import im.vector.app.EmojiCompatWrapper
import im.vector.app.EmojiSpanify import im.vector.app.EmojiSpanify
import im.vector.app.config.analyticsConfig
import im.vector.app.core.dispatchers.CoroutineDispatchers import im.vector.app.core.dispatchers.CoroutineDispatchers
import im.vector.app.core.error.DefaultErrorFormatter import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock 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.AnalyticsTracker
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics 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.navigation.Navigator
import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.SharedPrefPinCodeStore 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.SharedPreferencesUiStateRepository
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import org.matrix.android.sdk.api.Matrix 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.AuthenticationService
import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.HomeServerHistoryService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
@ -107,8 +113,17 @@ object VectorStaticModule {
} }
@Provides @Provides
fun providesMatrix(context: Context): Matrix { fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
return Matrix.getInstance(context) return MatrixConfiguration(
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider
)
}
@Provides
@Singleton
fun providesMatrix(context: Context, configuration: MatrixConfiguration): Matrix {
return Matrix.createInstance(context, configuration)
} }
@Provides @Provides
@ -147,4 +162,16 @@ object VectorStaticModule {
fun providesCoroutineDispatchers(): CoroutineDispatchers { fun providesCoroutineDispatchers(): CoroutineDispatchers {
return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default) 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
}
} }

View File

@ -67,7 +67,7 @@ import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.AnalyticsTracker 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.analytics.screen.ScreenEvent
import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.consent.ConsentNotGivenHelper import im.vector.app.features.consent.ConsentNotGivenHelper
@ -97,7 +97,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
* Analytics * Analytics
* ========================================================================================== */ * ========================================================================================== */
protected var analyticsScreenName: Screen.ScreenName? = null protected var analyticsScreenName: MobileScreen.ScreenName? = null
private var screenEvent: ScreenEvent? = null private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker protected lateinit var analyticsTracker: AnalyticsTracker

View File

@ -38,7 +38,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.analytics.AnalyticsTracker 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.analytics.screen.ScreenEvent
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -53,7 +53,7 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
* Analytics * Analytics
* ========================================================================================== */ * ========================================================================================== */
protected var analyticsScreenName: Screen.ScreenName? = null protected var analyticsScreenName: MobileScreen.ScreenName? = null
private var screenEvent: ScreenEvent? = null private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker protected lateinit var analyticsTracker: AnalyticsTracker

View File

@ -44,7 +44,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.utils.ToolbarConfig import im.vector.app.core.utils.ToolbarConfig
import im.vector.app.features.analytics.AnalyticsTracker 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.analytics.screen.ScreenEvent
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
@ -58,7 +58,7 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
* Analytics * Analytics
* ========================================================================================== */ * ========================================================================================== */
protected var analyticsScreenName: Screen.ScreenName? = null protected var analyticsScreenName: MobileScreen.ScreenName? = null
private var screenEvent: ScreenEvent? = null private var screenEvent: ScreenEvent? = null
protected lateinit var analyticsTracker: AnalyticsTracker protected lateinit var analyticsTracker: AnalyticsTracker

View File

@ -35,6 +35,7 @@ import im.vector.app.R
import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.platform.PendingIntentCompat
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.settings.BackgroundSyncMode 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 org.matrix.android.sdk.internal.session.sync.job.SyncService
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -75,6 +76,9 @@ class VectorSyncService : SyncService() {
} }
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var matrix: Matrix
override fun provideMatrix() = matrix
override fun getDefaultSyncDelaySeconds() = BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS override fun getDefaultSyncDelaySeconds() = BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS

View File

@ -16,19 +16,18 @@
package im.vector.app.features.analytics.impl package im.vector.app.features.analytics.impl
import android.content.Context
import com.posthog.android.Options import com.posthog.android.Options
import com.posthog.android.PostHog import com.posthog.android.PostHog
import com.posthog.android.Properties import com.posthog.android.Properties
import im.vector.app.BuildConfig import im.vector.app.core.di.NamedGlobalScope
import im.vector.app.config.analyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.log.analyticsTag import im.vector.app.features.analytics.log.analyticsTag
import im.vector.app.features.analytics.plan.UserProperties import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.analytics.store.AnalyticsStore 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.Flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -41,15 +40,30 @@ private val IGNORED_OPTIONS: Options? = null
@Singleton @Singleton
class DefaultVectorAnalytics @Inject constructor( class DefaultVectorAnalytics @Inject constructor(
private val context: Context, postHogFactory: PostHogFactory,
private val analyticsStore: AnalyticsStore analyticsConfig: AnalyticsConfig,
private val analyticsStore: AnalyticsStore,
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@NamedGlobalScope private val globalScope: CoroutineScope
) : VectorAnalytics { ) : 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 // Cache for the store values
private var userConsent: Boolean? = null private var userConsent: Boolean? = null
private var analyticsId: String? = null private var analyticsId: String? = null
override fun init() {
observeUserConsent()
observeAnalyticsId()
}
override fun getUserConsent(): Flow<Boolean> { override fun getUserConsent(): Flow<Boolean> {
return analyticsStore.userConsentFlow return analyticsStore.userConsentFlow
} }
@ -82,13 +96,6 @@ class DefaultVectorAnalytics @Inject constructor(
setAnalyticsId("") setAnalyticsId("")
} }
override fun init() {
observeUserConsent()
observeAnalyticsId()
createAnalyticsClient()
}
@Suppress("EXPERIMENTAL_API_USAGE")
private fun observeAnalyticsId() { private fun observeAnalyticsId() {
getAnalyticsId() getAnalyticsId()
.onEach { id -> .onEach { id ->
@ -96,21 +103,20 @@ class DefaultVectorAnalytics @Inject constructor(
analyticsId = id analyticsId = id
identifyPostHog() identifyPostHog()
} }
.launchIn(GlobalScope) .launchIn(globalScope)
} }
private fun identifyPostHog() { private suspend fun identifyPostHog() {
val id = analyticsId ?: return val id = analyticsId ?: return
if (id.isEmpty()) { if (id.isEmpty()) {
Timber.tag(analyticsTag.value).d("reset") Timber.tag(analyticsTag.value).d("reset")
posthog?.reset() posthog?.reset()
} else { } else {
Timber.tag(analyticsTag.value).d("identify") 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() { private fun observeUserConsent() {
getUserConsent() getUserConsent()
.onEach { consent -> .onEach { consent ->
@ -118,49 +124,13 @@ class DefaultVectorAnalytics @Inject constructor(
userConsent = consent userConsent = consent
optOutPostHog() optOutPostHog()
} }
.launchIn(GlobalScope) .launchIn(globalScope)
} }
private fun optOutPostHog() { private fun optOutPostHog() {
userConsent?.let { posthog?.optOut(!it) } 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) { override fun capture(event: VectorAnalyticsEvent) {
Timber.tag(analyticsTag.value).d("capture($event)") Timber.tag(analyticsTag.value).d("capture($event)")
posthog posthog

View File

@ -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())
}
}
}

View File

@ -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
}
}
}

View File

@ -22,9 +22,9 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
// https://github.com/matrix-org/matrix-analytics-events/ // 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. * How long the screen was displayed for in milliseconds.
*/ */
@ -33,6 +33,11 @@ data class Screen(
) : VectorAnalyticsScreen { ) : VectorAnalyticsScreen {
enum class ScreenName { enum class ScreenName {
/**
* The screen that displays the user's breadcrumbs.
*/
Breadcrumbs,
/** /**
* The screen shown to create a new (non-direct) room. * The screen shown to create a new (non-direct) room.
*/ */
@ -43,6 +48,16 @@ data class Screen(
*/ */
DeactivateAccount, 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 * The form for the forgot password use case
*/ */
@ -54,11 +69,15 @@ data class Screen(
Group, Group,
/** /**
* The Home tab on iOS | possibly the same on Android? | Home page on * The Home tab on iOS | possibly the same on Android?
* Web
*/ */
Home, 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 * The screen that displays the login flow (when the user already has an
* account). * account).
@ -66,100 +85,14 @@ data class Screen(
Login, Login,
/** /**
* The screen that displays the user's breadcrumbs. * Legacy: The screen that shows all groups/communities you have joined.
*/ */
MobileBreadcrumbs, MyGroups,
/**
* 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,
/** /**
* The People tab on mobile that lists all the DM rooms you have joined. * The People tab on mobile that lists all the DM rooms you have joined.
*/ */
MobilePeople, People,
/**
* 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,
/** /**
* The screen that displays the registration flow (when the user wants * The screen that displays the registration flow (when the user wants
@ -216,107 +149,87 @@ data class Screen(
*/ */
RoomUploads, 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 * Screen that displays the list of rooms and spaces of a space
*/ */
SpaceExploreRooms, 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. * The screen shown to create a new direct room.
*/ */
StartChat, StartChat,
/**
* The screen shown to select which room directory you'd like to use.
*/
SwitchDirectory,
/** /**
* A screen that shows information about a room member. * A screen that shows information about a room member.
*/ */
User, 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. * The splash screen.
*/ */

View File

@ -18,13 +18,13 @@ package im.vector.app.features.analytics.screen
import android.os.SystemClock import android.os.SystemClock
import im.vector.app.features.analytics.AnalyticsTracker 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 import timber.log.Timber
/** /**
* Track a screen display. Unique usage. * Track a screen display. Unique usage.
*/ */
class ScreenEvent(val screenName: Screen.ScreenName) { class ScreenEvent(val screenName: MobileScreen.ScreenName) {
private val startTime = SystemClock.elapsedRealtime() private val startTime = SystemClock.elapsedRealtime()
// Protection to avoid multiple sending // 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 * @param screenNameOverride can be used to override the screen name passed in constructor parameter
*/ */
fun send(analyticsTracker: AnalyticsTracker, fun send(analyticsTracker: AnalyticsTracker,
screenNameOverride: Screen.ScreenName? = null) { screenNameOverride: MobileScreen.ScreenName? = null) {
if (isSent) { if (isSent) {
Timber.w("Event $screenName Already sent!") Timber.w("Event $screenName Already sent!")
return return
} }
isSent = true isSent = true
analyticsTracker.screen( analyticsTracker.screen(
Screen( MobileScreen(
screenName = screenNameOverride ?: screenName, screenName = screenNameOverride ?: screenName,
durationMs = (SystemClock.elapsedRealtime() - startTime).toInt() durationMs = (SystemClock.elapsedRealtime() - startTime).toInt()
) )

View File

@ -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)
}
}

View File

@ -16,31 +16,81 @@
package im.vector.app.features.autocomplete.member package im.vector.app.features.autocomplete.member
import android.content.Context
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener 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.autocomplete.autocompleteMatrixItem
import im.vector.app.features.home.AvatarRenderer 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 org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject 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 @Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<RoomMemberSummary>?) { /* ==========================================================================================
* Specialization
* ========================================================================================== */
override fun buildModels(data: List<AutocompleteMemberItem>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return 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 val host = this
data.forEach { user ->
autocompleteMatrixItem { autocompleteMatrixItem {
roomMember.roomMemberSummary.let { user ->
id(user.userId) id(user.userId)
matrixItem(user.toMatrixItem()) matrixItem(user.toMatrixItem())
avatarRenderer(host.avatarRenderer) 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) }
} }
} }
} }

View File

@ -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()
}

View File

@ -21,26 +21,44 @@ import androidx.recyclerview.widget.RecyclerView
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter 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.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session 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.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership 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.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.MatrixItem
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
@Assisted val roomId: String, @Assisted val roomId: String,
session: Session, private val session: Session,
private val controller: AutocompleteMemberController private val controller: AutocompleteMemberController
) : RecyclerViewPresenter<RoomMemberSummary>(context), AutocompleteClickListener<RoomMemberSummary> { ) : RecyclerViewPresenter<AutocompleteMemberItem>(context), AutocompleteClickListener<AutocompleteMemberItem> {
/* ==========================================================================================
* Fields
* ========================================================================================== */
private val room by lazy { session.getRoom(roomId)!! } private val room by lazy { session.getRoom(roomId)!! }
/* ==========================================================================================
* Init
* ========================================================================================== */
init { init {
controller.listener = this controller.listener = this
} }
/* ==========================================================================================
* Public api
* ========================================================================================== */
fun clear() { fun clear() {
controller.listener = null controller.listener = null
} }
@ -50,16 +68,48 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
fun create(roomId: String): AutocompleteMemberPresenter fun create(roomId: String): AutocompleteMemberPresenter
} }
/* ==========================================================================================
* Specialization
* ========================================================================================== */
override fun instantiateAdapter(): RecyclerView.Adapter<*> { override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return controller.adapter return controller.adapter
} }
override fun onItemClick(t: RoomMemberSummary) { override fun onItemClick(t: AutocompleteMemberItem) {
dispatchClick(t) dispatchClick(t)
} }
override fun onQuery(query: CharSequence?) { override fun onQuery(query: CharSequence?) {
val queryParams = roomMemberQueryParams { 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)
}
}
controller.setData(items)
}
/* ==========================================================================================
* Helper methods
* ========================================================================================== */
private fun createQueryParams(query: CharSequence?) = roomMemberQueryParams {
displayName = if (query.isNullOrBlank()) { displayName = if (query.isNullOrBlank()) {
QueryStringValue.IsNotEmpty QueryStringValue.IsNotEmpty
} else { } else {
@ -68,11 +118,50 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
memberships = listOf(Membership.JOIN) memberships = listOf(Membership.JOIN)
excludeSelf = true excludeSelf = true
} }
val members = room.getRoomMembers(queryParams)
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() .asSequence()
.sortedBy { it.displayName } .sortedBy { it.displayName }
.disambiguate() .disambiguate()
controller.setData(members.toList()) .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"
} }
} }

View File

@ -54,6 +54,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.ActivityCallBinding import im.vector.app.databinding.ActivityCallBinding
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
import im.vector.app.features.call.dialpad.DialPadFragment 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.utils.EglUtils
import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
@ -165,6 +166,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
?.let { ?.let {
callViewModel.handle(VectorCallViewActions.SwitchCall(it)) callViewModel.handle(VectorCallViewActions.SwitchCall(it))
} }
this.intent = intent
} }
override fun getMenuRes() = R.menu.vector_call override fun getMenuRes() = R.menu.vector_call
@ -522,15 +524,22 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
val callId = withState(callViewModel) { it.callId } val callId = withState(callViewModel) { it.callId }
navigator.openCallTransfer(this, callTransferActivityResultLauncher, callId) navigator.openCallTransfer(this, callTransferActivityResultLauncher, callId)
} }
is VectorCallViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
null -> { null -> {
} }
} }
} }
private val callTransferActivityResultLauncher = registerStartForActivityResult { activityResult -> private val callTransferActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_CANCELED) { when (activityResult.resultCode) {
Activity.RESULT_CANCELED -> {
callViewModel.handle(VectorCallViewActions.CallTransferSelectionCancelled) callViewModel.handle(VectorCallViewActions.CallTransferSelectionCancelled)
} }
Activity.RESULT_OK -> {
CallTransferActivity.getCallTransferResult(activityResult.data)
?.let { callViewModel.handle(VectorCallViewActions.CallTransferSelectionResult(it)) }
}
}
} }
private fun onErrorTimoutConnect(turn: TurnServerResponse?) { private fun onErrorTimoutConnect(turn: TurnServerResponse?) {

View File

@ -18,6 +18,7 @@ package im.vector.app.features.call
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.transfer.CallTransferResult
sealed class VectorCallViewActions : VectorViewModelAction { sealed class VectorCallViewActions : VectorViewModelAction {
object EndCall : VectorCallViewActions() object EndCall : VectorCallViewActions()
@ -37,5 +38,6 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object ToggleHDSD : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions()
object InitiateCallTransfer : VectorCallViewActions() object InitiateCallTransfer : VectorCallViewActions()
object CallTransferSelectionCancelled : VectorCallViewActions() object CallTransferSelectionCancelled : VectorCallViewActions()
data class CallTransferSelectionResult(val callTransferResult: CallTransferResult) : VectorCallViewActions()
object TransferCall : VectorCallViewActions() object TransferCall : VectorCallViewActions()
} }

View File

@ -29,6 +29,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
) : VectorCallViewEvents() ) : VectorCallViewEvents()
object ShowDialPad : VectorCallViewEvents() object ShowDialPad : VectorCallViewEvents()
object ShowCallTransferScreen : VectorCallViewEvents() object ShowCallTransferScreen : VectorCallViewEvents()
object FailToTransfer : VectorCallViewEvents()
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents() // data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents() // data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
// object CallAccepted : VectorCallViewEvents() // object CallAccepted : VectorCallViewEvents()

View File

@ -29,13 +29,17 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.audio.CallAudioManager 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.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem
import im.vector.app.features.createdirect.DirectRoomHelper
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns 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.Session
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
@ -47,7 +51,9 @@ class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState, @Assisted initialState: VectorCallViewState,
val session: Session, val session: Session,
val callManager: WebRtcCallManager, val callManager: WebRtcCallManager,
val proximityManager: CallProximityManager val proximityManager: CallProximityManager,
private val dialPadLookup: DialPadLookup,
private val directRoomHelper: DirectRoomHelper,
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) { ) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
private var call: WebRtcCall? = null private var call: WebRtcCall? = null
@ -327,6 +333,9 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewActions.CallTransferSelectionCancelled -> { VectorCallViewActions.CallTransferSelectionCancelled -> {
call?.updateRemoteOnHold(false) call?.updateRemoteOnHold(false)
} }
is VectorCallViewActions.CallTransferSelectionResult -> {
handleCallTransferSelectionResult(action.callTransferResult)
}
VectorCallViewActions.TransferCall -> { VectorCallViewActions.TransferCall -> {
handleCallTransfer() 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 @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<VectorCallViewModel, VectorCallViewState> { interface Factory : MavericksAssistedViewModelFactory<VectorCallViewModel, VectorCallViewState> {
override fun create(initialState: VectorCallViewState): VectorCallViewModel override fun create(initialState: VectorCallViewState): VectorCallViewModel

View File

@ -40,7 +40,7 @@ import com.android.dialer.dialpadview.DigitsEditText
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.features.analytics.AnalyticsTracker 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.analytics.screen.ScreenEvent
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -69,7 +69,7 @@ class DialPadFragment : Fragment(), TextWatcher {
private var screenEvent: ScreenEvent? = null private var screenEvent: ScreenEvent? = null
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
screenEvent = ScreenEvent(Screen.ScreenName.MobileDialpad) screenEvent = ScreenEvent(MobileScreen.ScreenName.Dialpad)
} }
override fun onPause() { override fun onPause() {

View File

@ -16,7 +16,6 @@
package im.vector.app.features.call.transfer package im.vector.app.features.call.transfer
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -27,6 +26,7 @@ import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter 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.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityCallTransferBinding import im.vector.app.databinding.ActivityCallTransferBinding
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -57,9 +57,7 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>() {
callTransferViewModel.observeViewEvents { callTransferViewModel.observeViewEvents {
when (it) { when (it) {
is CallTransferViewEvents.Complete -> handleComplete() is CallTransferViewEvents.Complete -> handleComplete()
CallTransferViewEvents.Loading -> showWaitingView() }.exhaustive
is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
}
} }
sectionsPagerAdapter = CallTransferPagerAdapter(this) sectionsPagerAdapter = CallTransferPagerAdapter(this)
@ -82,29 +80,41 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>() {
when (views.callTransferTabLayout.selectedTabPosition) { when (views.callTransferTabLayout.selectedTabPosition) {
CallTransferPagerAdapter.USER_LIST_INDEX -> { CallTransferPagerAdapter.USER_LIST_INDEX -> {
val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull() ?: return@debouncedClicks val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull() ?: return@debouncedClicks
val action = CallTransferAction.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser) val result = CallTransferResult.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
callTransferViewModel.handle(action) handleComplete(result)
} }
CallTransferPagerAdapter.DIAL_PAD_INDEX -> { CallTransferPagerAdapter.DIAL_PAD_INDEX -> {
val phoneNumber = sectionsPagerAdapter.dialPadFragment?.getRawInput() ?: return@debouncedClicks val phoneNumber = sectionsPagerAdapter.dialPadFragment?.getRawInput() ?: return@debouncedClicks
val action = CallTransferAction.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber) val result = CallTransferResult.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
callTransferViewModel.handle(action) handleComplete(result)
} }
} }
} }
} }
private fun handleComplete() { private fun handleComplete(callTransferResult: CallTransferResult? = null) {
setResult(Activity.RESULT_OK) if (callTransferResult != null) {
val intent = Intent().apply {
putExtra(EXTRA_TRANSFER_RESULT, callTransferResult)
}
setResult(RESULT_OK, intent)
} else {
setResult(RESULT_OK)
}
finish() finish()
} }
companion object { companion object {
private const val EXTRA_TRANSFER_RESULT = "EXTRA_TRANSFER_RESULT"
fun newIntent(context: Context, callId: String): Intent { fun newIntent(context: Context, callId: String): Intent {
return Intent(context, CallTransferActivity::class.java).also { return Intent(context, CallTransferActivity::class.java).also {
it.putExtra(Mavericks.KEY_ARG, CallTransferArgs(callId)) it.putExtra(Mavericks.KEY_ARG, CallTransferArgs(callId))
} }
} }
fun getCallTransferResult(intent: Intent?): CallTransferResult? {
return intent?.extras?.getParcelable(EXTRA_TRANSFER_RESULT)
}
} }
} }

View File

@ -16,9 +16,10 @@
package im.vector.app.features.call.transfer 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 { sealed class CallTransferResult : Parcelable {
data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction() @Parcelize data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferResult()
data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction() @Parcelize data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferResult()
} }

View File

@ -20,6 +20,4 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class CallTransferViewEvents : VectorViewEvents { sealed class CallTransferViewEvents : VectorViewEvents {
object Complete : CallTransferViewEvents() object Complete : CallTransferViewEvents()
object Loading : CallTransferViewEvents()
object FailToTransfer : CallTransferViewEvents()
} }

View File

@ -22,22 +22,16 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory 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.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.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager 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.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
private val dialPadLookup: DialPadLookup,
private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager) : private val callManager: WebRtcCallManager) :
VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) { VectorViewModel<CallTransferViewState, EmptyAction, CallTransferViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<CallTransferViewModel, CallTransferViewState> { interface Factory : MavericksAssistedViewModelFactory<CallTransferViewModel, CallTransferViewState> {
@ -68,53 +62,5 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
call?.removeListener(callListener) call?.removeListener(callListener)
} }
override fun handle(action: CallTransferAction) { override fun handle(action: EmptyAction) { }
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)
}
}
}
} }

View File

@ -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.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.core.utils.registerForPermissionsResult 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.contactsbook.ContactsBookFragment
import im.vector.app.features.qrcode.QrCodeScannerEvents import im.vector.app.features.qrcode.QrCodeScannerEvents
import im.vector.app.features.qrcode.QrCodeScannerFragment import im.vector.app.features.qrcode.QrCodeScannerFragment
@ -71,7 +71,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.StartChat analyticsScreenName = MobileScreen.ScreenName.StartChat
views.toolbar.visibility = View.GONE views.toolbar.visibility = View.GONE
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)

View File

@ -48,7 +48,7 @@ import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel 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.analytics.screen.ScreenEvent
import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
@ -165,7 +165,7 @@ class HomeActivity :
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
private var drawerScreenEvent: ScreenEvent? = null private var drawerScreenEvent: ScreenEvent? = null
override fun onDrawerOpened(drawerView: View) { override fun onDrawerOpened(drawerView: View) {
drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileSidebar) drawerScreenEvent = ScreenEvent(MobileScreen.ScreenName.Sidebar)
} }
override fun onDrawerClosed(drawerView: View) { override fun onDrawerClosed(drawerView: View) {
@ -184,7 +184,7 @@ class HomeActivity :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.Home analyticsScreenName = MobileScreen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice()) FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java) sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)

View File

@ -457,7 +457,7 @@ class HomeDetailFragment @Inject constructor(
backgroundColor = if (highlight) { backgroundColor = if (highlight) {
ThemeUtils.getColor(requireContext(), R.attr.colorError) ThemeUtils.getColor(requireContext(), R.attr.colorError)
} else { } else {
ThemeUtils.getColor(requireContext(), R.attr.vctr_unread_room_badge) ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary)
} }
} }

View File

@ -30,7 +30,7 @@ import im.vector.app.core.extensions.replaceChildFragment
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentHomeDrawerBinding 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.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.spaces.SpaceListFragment import im.vector.app.features.spaces.SpaceListFragment
@ -98,7 +98,7 @@ class HomeDrawerFragment @Inject constructor(
views.homeDrawerInviteFriendButton.debouncedClicks { views.homeDrawerInviteFriendButton.debouncedClicks {
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink -> 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) val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent( startSharePlainTextIntent(

View File

@ -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.command.CommandAutocompletePolicy
import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter
import im.vector.app.features.autocomplete.group.AutocompleteGroupPresenter 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.member.AutocompleteMemberPresenter
import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.app.features.command.Command 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.html.PillImageSpan
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.group.model.GroupSummary 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.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem 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.toMatrixItem
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
@ -106,7 +107,7 @@ class AutoCompleter @AssistedInject constructor(
Autocomplete.on<Command>(editText) Autocomplete.on<Command>(editText)
.with(commandAutocompletePolicy) .with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter) .with(autocompleteCommandPresenter)
.with(ELEVATION) .with(ELEVATION_DP)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> { .with(object : AutocompleteCallback<Command> {
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
@ -125,15 +126,24 @@ class AutoCompleter @AssistedInject constructor(
private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) { private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId) autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
Autocomplete.on<RoomMemberSummary>(editText) Autocomplete.on<AutocompleteMemberItem>(editText)
.with(CharPolicy('@', true)) .with(CharPolicy(TRIGGER_AUTO_COMPLETE_MEMBERS, true))
.with(autocompleteMemberPresenter) .with(autocompleteMemberPresenter)
.with(ELEVATION) .with(ELEVATION_DP)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomMemberSummary> { .with(object : AutocompleteCallback<AutocompleteMemberItem> {
override fun onPopupItemClicked(editable: Editable, item: RoomMemberSummary): Boolean { override fun onPopupItemClicked(editable: Editable, item: AutocompleteMemberItem): Boolean {
insertMatrixItem(editText, editable, "@", item.toMatrixItem()) return when (item) {
return true 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) { override fun onPopupVisibilityChanged(shown: Boolean) {
@ -144,13 +154,13 @@ class AutoCompleter @AssistedInject constructor(
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) { private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
Autocomplete.on<RoomSummary>(editText) Autocomplete.on<RoomSummary>(editText)
.with(CharPolicy('#', true)) .with(CharPolicy(TRIGGER_AUTO_COMPLETE_ROOMS, true))
.with(autocompleteRoomPresenter) .with(autocompleteRoomPresenter)
.with(ELEVATION) .with(ELEVATION_DP)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomSummary> { .with(object : AutocompleteCallback<RoomSummary> {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem()) insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_ROOMS, item.toRoomAliasMatrixItem())
return true return true
} }
@ -162,13 +172,13 @@ class AutoCompleter @AssistedInject constructor(
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) { private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
Autocomplete.on<GroupSummary>(editText) Autocomplete.on<GroupSummary>(editText)
.with(CharPolicy('+', true)) .with(CharPolicy(TRIGGER_AUTO_COMPLETE_GROUPS, true))
.with(autocompleteGroupPresenter) .with(autocompleteGroupPresenter)
.with(ELEVATION) .with(ELEVATION_DP)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<GroupSummary> { .with(object : AutocompleteCallback<GroupSummary> {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
insertMatrixItem(editText, editable, "+", item.toMatrixItem()) insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_GROUPS, item.toMatrixItem())
return true return true
} }
@ -180,9 +190,9 @@ class AutoCompleter @AssistedInject constructor(
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) { private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
Autocomplete.on<String>(editText) Autocomplete.on<String>(editText)
.with(CharPolicy(':', false)) .with(CharPolicy(TRIGGER_AUTO_COMPLETE_EMOJIS, false))
.with(autocompleteEmojiPresenter) .with(autocompleteEmojiPresenter)
.with(ELEVATION) .with(ELEVATION_DP)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<String> { .with(object : AutocompleteCallback<String> {
override fun onPopupItemClicked(editable: Editable, item: String): Boolean { override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
@ -210,7 +220,7 @@ class AutoCompleter @AssistedInject constructor(
.build() .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 // Detect last firstChar and remove it
var startIndex = editable.lastIndexOf(firstChar) var startIndex = editable.lastIndexOf(firstChar)
if (startIndex == -1) { if (startIndex == -1) {
@ -228,7 +238,7 @@ class AutoCompleter @AssistedInject constructor(
// Adding trailing space " " or ": " if the user started mention someone // Adding trailing space " " or ": " if the user started mention someone
val displayNameSuffix = val displayNameSuffix =
if (firstChar == "@" && startIndex == 0) { if (matrixItem is MatrixItem.UserItem) {
": " ": "
} else { } else {
" " " "
@ -249,6 +259,10 @@ class AutoCompleter @AssistedInject constructor(
} }
companion object { 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 = ':'
} }
} }

View File

@ -35,7 +35,7 @@ import im.vector.app.core.extensions.keepScreenOn
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityRoomDetailBinding 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.analytics.screen.ScreenEvent
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.arguments.TimelineArgs
@ -160,7 +160,7 @@ class RoomDetailActivity :
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
private var drawerScreenEvent: ScreenEvent? = null private var drawerScreenEvent: ScreenEvent? = null
override fun onDrawerOpened(drawerView: View) { override fun onDrawerOpened(drawerView: View) {
drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileBreadcrumbs) drawerScreenEvent = ScreenEvent(MobileScreen.ScreenName.Breadcrumbs)
} }
override fun onDrawerClosed(drawerView: View) { override fun onDrawerClosed(drawerView: View) {

View File

@ -29,7 +29,7 @@ import java.io.File
* Transient events for RoomDetail * Transient events for RoomDetail
*/ */
sealed class RoomDetailViewEvents : VectorViewEvents { 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 OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents() data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents()

View File

@ -49,6 +49,7 @@ data class JitsiState(
data class RoomDetailViewState( data class RoomDetailViewState(
val roomId: String, val roomId: String,
val eventId: String?, val eventId: String?,
val isInviteAlreadyAccepted: Boolean,
val myRoomMember: Async<RoomMemberSummary> = Uninitialized, val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
val asyncInviter: Async<RoomMemberSummary> = Uninitialized, val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
@ -77,6 +78,7 @@ data class RoomDetailViewState(
constructor(args: TimelineArgs) : this( constructor(args: TimelineArgs) : this(
roomId = args.roomId, roomId = args.roomId,
eventId = args.eventId, eventId = args.eventId,
isInviteAlreadyAccepted = args.isInviteAlreadyAccepted,
// Also highlight the target event, if any // Also highlight the target event, if any
highlightedEventId = args.eventId, highlightedEventId = args.eventId,
switchToParentSpace = args.switchToParentSpace, switchToParentSpace = args.switchToParentSpace,

View File

@ -120,7 +120,7 @@ import im.vector.app.core.utils.toast
import im.vector.app.databinding.DialogReportContentBinding import im.vector.app.databinding.DialogReportContentBinding
import im.vector.app.databinding.FragmentTimelineBinding import im.vector.app.databinding.FragmentTimelineBinding
import im.vector.app.features.analytics.plan.Composer 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.AttachmentTypeSelectorView
import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.AttachmentsHelper
import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.ContactAttachment
@ -342,7 +342,7 @@ class TimelineFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.Room analyticsScreenName = MobileScreen.ScreenName.Room
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle -> setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId -> bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
timelineViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) timelineViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
@ -448,7 +448,7 @@ class TimelineFragment @Inject constructor(
timelineViewModel.observeViewEvents { timelineViewModel.observeViewEvents {
when (it) { when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) is RoomDetailViewEvents.Failure -> displayErrorMessage(it)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it) is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(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) { private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
val tag = RoomWidgetPermissionBottomSheet::class.java.name val tag = RoomWidgetPermissionBottomSheet::class.java.name
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
@ -2374,12 +2378,10 @@ class TimelineFragment @Inject constructor(
// VectorInviteView.Callback // VectorInviteView.Callback
override fun onAcceptInvite() { override fun onAcceptInvite() {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
timelineViewModel.handle(RoomDetailAction.AcceptInvite) timelineViewModel.handle(RoomDetailAction.AcceptInvite)
} }
override fun onRejectInvite() { override fun onRejectInvite() {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
timelineViewModel.handle(RoomDetailAction.RejectInvite) timelineViewModel.handle(RoomDetailAction.RejectInvite)
} }

View File

@ -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.factory.TimelineFactory
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever 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.home.room.typing.TypingHelper
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorDataStore import im.vector.app.features.settings.VectorDataStore
@ -123,6 +124,7 @@ class TimelineViewModel @AssistedInject constructor(
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val activeConferenceHolder: JitsiActiveConferenceHolder, private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val decryptionFailureTracker: DecryptionFailureTracker, private val decryptionFailureTracker: DecryptionFailureTracker,
private val notificationDrawerManager: NotificationDrawerManager,
timelineFactory: TimelineFactory, timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler appStateHandler: AppStateHandler
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
@ -193,6 +195,11 @@ class TimelineViewModel @AssistedInject constructor(
prepareForEncryption() prepareForEncryption()
} }
// If the user had already accepted the invitation in the room list
if (initialState.isInviteAlreadyAccepted) {
handleAcceptInvite()
}
if (initialState.switchToParentSpace) { if (initialState.switchToParentSpace) {
// We are coming from a notification, try to switch to the most relevant space // 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 // so that when hitting back the room will appear in the list
@ -803,16 +810,24 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleRejectInvite() { private fun handleRejectInvite() {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch { 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() { private fun handleAcceptInvite() {
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
viewModelScope.launch { viewModelScope.launch {
tryOrNull { try {
session.joinRoom(room.roomId) session.joinRoom(room.roomId)
analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom()) analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom())
} catch (throwable: Throwable) {
_viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
} }
} }
} }

View File

@ -28,5 +28,6 @@ data class TimelineArgs(
val sharedData: SharedData? = null, val sharedData: SharedData? = null,
val openShareSpaceForId: String? = null, val openShareSpaceForId: String? = null,
val threadTimelineArgs: ThreadTimelineArgs? = null, val threadTimelineArgs: ThreadTimelineArgs? = null,
val switchToParentSpace: Boolean = false val switchToParentSpace: Boolean = false,
val isInviteAlreadyAccepted: Boolean = false
) : Parcelable ) : Parcelable

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.factory 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.di.ActiveSessionHolder
import im.vector.app.core.extensions.prevOrNull import im.vector.app.core.extensions.prevOrNull
import im.vector.app.features.home.AvatarRenderer 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.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration 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.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.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 im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
@ -82,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
event: TimelineEvent, event: TimelineEvent,
eventIdToHighlight: String?, eventIdToHighlight: String?,
requestModelBuild: () -> Unit, requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { callback: TimelineEventController.Callback?): MergedSimilarEventsItem_? {
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents( val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
items, items,
currentPosition, currentPosition,
@ -122,7 +123,14 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
collapsedEventIds.removeAll(mergedEventIds) collapsedEventIds.removeAll(mergedEventIds)
} }
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedMembershipEventsItem.Attributes( 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, isCollapsed = isCollapsed,
mergeData = mergedData, mergeData = mergedData,
avatarRenderer = avatarRenderer, avatarRenderer = avatarRenderer,
@ -131,7 +139,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
requestModelBuild() requestModelBuild()
} }
) )
MergedMembershipEventsItem_() MergedSimilarEventsItem_()
.id(mergeId) .id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted) .highlighted(isCollapsed && highlighted)
@ -141,6 +149,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
} }
} }
} }
}
private fun buildRoomCreationMergedSummary(currentPosition: Int, private fun buildRoomCreationMergedSummary(currentPosition: Int,
items: List<TimelineEvent>, items: List<TimelineEvent>,

View File

@ -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.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.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.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
@ -112,6 +113,7 @@ class MessageItemFactory @Inject constructor(
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>, private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val htmlCompressor: VectorHtmlCompressor, private val htmlCompressor: VectorHtmlCompressor,
private val textRendererFactory: EventTextRenderer.Factory,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val imageContentRenderer: ImageContentRenderer, private val imageContentRenderer: ImageContentRenderer,
private val messageInformationDataFactory: MessageInformationDataFactory, private val messageInformationDataFactory: MessageInformationDataFactory,
@ -138,6 +140,10 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId) pillsPostProcessorFactory.create(roomId)
} }
private val textRenderer by lazy {
textRendererFactory.create(roomId)
}
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event val event = params.event
val highlight = params.isHighlighted val highlight = params.isHighlighted
@ -549,8 +555,9 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes): MessageTextItem? {
val bindingOptions = spanUtils.getBindingOptions(body) val renderedBody = textRenderer.render(body)
val linkifiedBody = body.linkify(callback) val bindingOptions = spanUtils.getBindingOptions(renderedBody)
val linkifiedBody = renderedBody.linkify(callback)
return MessageTextItem_() return MessageTextItem_()
.message( .message(

View File

@ -56,7 +56,8 @@ object TimelineDisplayableEvents {
} }
fun TimelineEvent.canBeMerged(): Boolean { 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 { fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {

View File

@ -20,6 +20,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.PluralsRes
import androidx.core.view.children import androidx.core.view.children
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
@ -27,7 +28,7 @@ import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) @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 override fun getViewStubId() = STUB_ID
@ -37,7 +38,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
if (attributes.isCollapsed) { 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.text = summary
holder.summaryView.visibility = View.VISIBLE holder.summaryView.visibility = View.VISIBLE
holder.avatarListView.visibility = View.VISIBLE holder.avatarListView.visibility = View.VISIBLE
@ -66,6 +67,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
} }
data class Attributes( data class Attributes(
@PluralsRes val summaryTitleResId: Int,
override val isCollapsed: Boolean, override val isCollapsed: Boolean,
override val mergeData: List<Data>, override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,

View File

@ -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)
}
}

View File

@ -24,7 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityFilteredRoomsBinding 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.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.RoomListParams
@ -43,7 +43,7 @@ class FilteredRoomsActivity : VectorBaseActivity<ActivityFilteredRoomsBinding>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = Screen.ScreenName.RoomFilter analyticsScreenName = MobileScreen.ScreenName.RoomFilter
setupToolbar(views.filteredRoomsToolbar) setupToolbar(views.filteredRoomsToolbar)
.allowBack() .allowBack()
if (isFirstCreation()) { if (isFirstCreation()) {

View File

@ -42,7 +42,7 @@ import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.databinding.FragmentRoomListBinding 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.RoomListDisplayMode
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
@ -104,8 +104,8 @@ class RoomListFragment @Inject constructor(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
analyticsScreenName = when (roomListParams.displayMode) { analyticsScreenName = when (roomListParams.displayMode) {
RoomListDisplayMode.PEOPLE -> Screen.ScreenName.MobilePeople RoomListDisplayMode.PEOPLE -> MobileScreen.ScreenName.People
RoomListDisplayMode.ROOMS -> Screen.ScreenName.MobileRooms RoomListDisplayMode.ROOMS -> MobileScreen.ScreenName.Rooms
else -> null else -> null
} }
} }
@ -121,7 +121,7 @@ class RoomListFragment @Inject constructor(
when (it) { when (it) {
is RoomListViewEvents.Loading -> showLoading(it.message) is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable) is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it) is RoomListViewEvents.SelectRoom -> handleSelectRoom(it, it.isInviteAlreadyAccepted)
is RoomListViewEvents.Done -> Unit is RoomListViewEvents.Done -> Unit
is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link) is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
}.exhaustive }.exhaustive
@ -184,8 +184,8 @@ class RoomListFragment @Inject constructor(
super.onDestroyView() super.onDestroyView()
} }
private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) { private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom, isInviteAlreadyAccepted: Boolean) {
navigator.openRoom(requireActivity(), event.roomSummary.roomId) navigator.openRoom(context = requireActivity(), roomId = event.roomSummary.roomId, isInviteAlreadyAccepted = isInviteAlreadyAccepted)
} }
private fun setupCreateRoomButton() { private fun setupCreateRoomButton() {

View File

@ -27,7 +27,7 @@ sealed class RoomListViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomListViewEvents() data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
data class Failure(val throwable: Throwable) : 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() object Done : RoomListViewEvents()
data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents() data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents()
} }

View File

@ -33,7 +33,6 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.analytics.AnalyticsTracker 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.displayname.getBestName
import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@ -174,7 +173,7 @@ class RoomListViewModel @AssistedInject constructor(
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState { 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) { 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") Timber.w("Try to join an already joining room. Should not happen")
return@withState return@withState
} }
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary, true))
// quick echo // quick echo
setState { 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 -> private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->

View File

@ -19,6 +19,7 @@
package im.vector.app.features.html package im.vector.app.features.html
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.drawable.Drawable 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.core.glide.GlideRequests
import im.vector.app.features.displayname.getBestName import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer 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.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -117,6 +119,11 @@ class PillImageSpan(private val glideRequests: GlideRequests,
setChipMinHeightResource(R.dimen.pill_min_height) setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size) setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = icon 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) setBounds(0, 0, intrinsicWidth, intrinsicHeight)
} }
} }

View File

@ -38,55 +38,85 @@ class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomI
private val sessionHolder: ActiveSessionHolder) : private val sessionHolder: ActiveSessionHolder) :
EventHtmlRenderer.PostProcessor { EventHtmlRenderer.PostProcessor {
/* ==========================================================================================
* Public api
* ========================================================================================== */
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String?): PillsPostProcessor fun create(roomId: String?): PillsPostProcessor
} }
/* ==========================================================================================
* Specialization
* ========================================================================================== */
override fun afterRender(renderedText: Spannable) { override fun afterRender(renderedText: Spannable) {
addPillSpans(renderedText, roomId) addPillSpans(renderedText, roomId)
} }
/* ==========================================================================================
* Helper methods
* ========================================================================================== */
private fun addPillSpans(renderedText: Spannable, roomId: String?) { 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. // We let markdown handle links and then we add PillImageSpan if needed.
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java) val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
linkSpans.forEach { linkSpan -> linkSpans.forEach { linkSpan ->
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
val startSpan = renderedText.getSpanStart(linkSpan) val startSpan = renderedText.getSpanStart(linkSpan)
val endSpan = renderedText.getSpanEnd(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? { private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
val permalinkData = PermalinkParser.parse(url) val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
val matrixItem = when (permalinkData) { is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId)
is PermalinkData.UserLink -> { is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
is PermalinkData.GroupLink -> permalinkData.toMatrixItem()
else -> null
} ?: return null
return createPillImageSpan(matrixItem)
}
private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? =
if (roomId == null) { if (roomId == null) {
sessionHolder.getSafeActiveSession()?.getUser(permalinkData.userId)?.toMatrixItem() sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()
} else { } else {
sessionHolder.getSafeActiveSession()?.getRoomMember(permalinkData.userId, roomId)?.toMatrixItem() sessionHolder.getSafeActiveSession()?.getRoomMember(userId, roomId)?.toMatrixItem()
} }
}
is PermalinkData.RoomLink -> { private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
if (permalinkData.eventId == null) { if (eventId == null) {
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(permalinkData.roomIdOrAlias) val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
if (permalinkData.isRoomAlias) { when {
MatrixItem.RoomAliasItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl) isRoomAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
} else { else -> MatrixItem.RoomItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
MatrixItem.RoomItem(permalinkData.roomIdOrAlias, room?.displayName, room?.avatarUrl)
} }
} else { } else {
// Exclude event link (used in reply events, we do not want to pill the "in reply to") // Exclude event link (used in reply events, we do not want to pill the "in reply to")
null null
} }
}
is PermalinkData.GroupLink -> { private fun PermalinkData.GroupLink.toMatrixItem(): MatrixItem? {
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(permalinkData.groupId) val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(groupId)
MatrixItem.GroupItem(permalinkData.groupId, group?.displayName, group?.avatarUrl) return MatrixItem.GroupItem(groupId, group?.displayName, group?.avatarUrl)
}
else -> null
} ?: return null
return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
} }
} }

View File

@ -38,7 +38,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityLoginBinding 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.home.HomeActivity
import im.vector.app.features.login.terms.LoginTermsFragment import im.vector.app.features.login.terms.LoginTermsFragment
import im.vector.app.features.login.terms.LoginTermsFragmentArgument 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 getCoordinatorLayout() = views.coordinatorLayout
override fun initUiAndData() { override fun initUiAndData() {
analyticsScreenName = Screen.ScreenName.Login analyticsScreenName = MobileScreen.ScreenName.Login
if (isFirstCreation()) { if (isFirstCreation()) {
addFirstFragment() addFirstFragment()
@ -203,7 +203,7 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
if (loginViewState.isUserLogged()) { if (loginViewState.isUserLogged()) {
if (loginViewState.signMode == SignMode.SignUp) { if (loginViewState.signMode == SignMode.SignUp) {
// change the screen name // change the screen name
analyticsScreenName = Screen.ScreenName.Register analyticsScreenName = MobileScreen.ScreenName.Register
} }
val intent = HomeActivity.newIntent( val intent = HomeActivity.newIntent(
this, this,

View File

@ -31,7 +31,7 @@ import im.vector.app.core.extensions.hidePassword
import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentLoginResetPasswordBinding 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.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -48,7 +48,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
private var showWarning = true private var showWarning = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
analyticsScreenName = Screen.ScreenName.ForgotPassword analyticsScreenName = MobileScreen.ScreenName.ForgotPassword
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }

View File

@ -26,7 +26,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.databinding.FragmentLoginSplashBinding 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 im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import java.net.UnknownHostException import java.net.UnknownHostException
@ -44,7 +44,7 @@ class LoginSplashFragment @Inject constructor(
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
analyticsScreenName = Screen.ScreenName.Welcome analyticsScreenName = MobileScreen.ScreenName.Welcome
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }

Some files were not shown because too many files have changed in this diff Show More