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
chmod 777 emulator.log
adb logcat >> emulator.log &
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 )
- name: Upload Test Report Log
uses: actions/upload-artifact@v2
if: always()
with:
name: sanity-error-results
path: |

View File

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

BIN
.idea/icon.png generated Normal file

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" />
<!-- Other colors, which are not in the palette -->
<attr name="vctr_unread_room_badge" format="color" />
<color name="vctr_unread_room_badge_light">@color/palette_gray_200</color>
<color name="vctr_unread_room_badge_dark">@color/palette_gray_250</color>
<color name="vctr_unread_room_badge_black">@color/palette_gray_250</color>
<attr name="vctr_fab_label_bg" format="color" />
<color name="vctr_fab_label_bg_light">@android:color/white</color>
<color name="vctr_fab_label_bg_dark">#FF181B21</color>

View File

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

View File

@ -7,7 +7,6 @@
<!-- Only setting the items we need to override to get the background to be pure black, otherwise inheriting -->
<!-- other colors -->
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_black</item>
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_black</item>
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_black</item>
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>

View File

@ -16,7 +16,6 @@
<item name="vctr_system">@color/element_system_dark</item>
<!-- other colors -->
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_dark</item>
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_dark</item>
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_dark</item>
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>

View File

@ -16,7 +16,6 @@
<item name="vctr_system">@color/element_system_light</item>
<!-- other colors -->
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_light</item>
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_light</item>
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_light</item>
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>

View File

@ -99,12 +99,31 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
private lateinit var instance: Matrix
private val isInit = AtomicBoolean(false)
/**
* Creates a new instance of Matrix, it's recommended to manage this instance as a singleton.
* To make use of the built in singleton use Matrix.initialize() and/or Matrix.getInstance(context) instead
**/
fun createInstance(context: Context, matrixConfiguration: MatrixConfiguration): Matrix {
return Matrix(context.applicationContext, matrixConfiguration)
}
/**
* Initializes a singleton instance of Matrix for the given MatrixConfiguration
* This instance will be returned by Matrix.getInstance(context)
*/
@Deprecated("Use Matrix.createInstance and manage the instance manually")
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
if (isInit.compareAndSet(false, true)) {
instance = Matrix(context.applicationContext, matrixConfiguration)
}
}
/**
* Either provides an already initialized singleton Matrix instance or queries the application context for a MatrixConfiguration.Provider
* to lazily create and store the instance.
*/
@Suppress("deprecation") // suppressing warning as this method is unused but is still provided for SDK clients
@Deprecated("Use Matrix.createInstance and manage the instance manually")
fun getInstance(context: Context): Matrix {
if (isInit.compareAndSet(false, true)) {
val appContext = context.applicationContext
@ -113,7 +132,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
instance = Matrix(appContext, matrixConfiguration)
} else {
throw IllegalStateException("Matrix is not initialized properly." +
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
" If you want to manage your own Matrix instance use Matrix.createInstance" +
" otherwise you should call Matrix.initialize or let your application implement MatrixConfiguration.Provider.")
}
}
return instance

View File

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

View File

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

View File

@ -36,7 +36,19 @@ sealed class MatrixItem(
data class UserItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
MatrixItem(id, displayName?.removeSuffix(IRC_PATTERN), avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
data class EveryoneInRoomItem(override val id: String,
override val displayName: String = NOTIFY_EVERYONE,
override val avatarUrl: String? = null,
val roomDisplayName: String? = null) :
MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@ -47,7 +59,7 @@ sealed class MatrixItem(
data class EventItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
MatrixItem(id, displayName, avatarUrl) {
MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@ -58,7 +70,7 @@ sealed class MatrixItem(
data class RoomItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
MatrixItem(id, displayName, avatarUrl) {
MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@ -69,7 +81,7 @@ sealed class MatrixItem(
data class SpaceItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
MatrixItem(id, displayName, avatarUrl) {
MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@ -80,7 +92,7 @@ sealed class MatrixItem(
data class RoomAliasItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
MatrixItem(id, displayName, avatarUrl) {
MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@ -91,7 +103,7 @@ sealed class MatrixItem(
data class GroupItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null) :
MatrixItem(id, displayName, avatarUrl) {
MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
@ -110,16 +122,22 @@ sealed class MatrixItem(
/**
* Return the prefix as defined in the matrix spec (and not extracted from the id)
*/
fun getIdPrefix() = when (this) {
is UserItem -> '@'
is EventItem -> '$'
private fun getIdPrefix() = when (this) {
is UserItem -> '@'
is EventItem -> '$'
is SpaceItem,
is RoomItem -> '!'
is RoomAliasItem -> '#'
is GroupItem -> '+'
is RoomItem,
is EveryoneInRoomItem -> '!'
is RoomAliasItem -> '#'
is GroupItem -> '+'
}
fun firstLetterOfDisplayName(): String {
val displayName = when (this) {
// use the room display name for the notify everyone item
is EveryoneInRoomItem -> roomDisplayName
else -> displayName
}
return (displayName?.takeIf { it.isNotBlank() } ?: id)
.let { dn ->
var startIndex = 0
@ -152,7 +170,8 @@ sealed class MatrixItem(
}
companion object {
private const val ircPattern = " (IRC)"
private const val IRC_PATTERN = " (IRC)"
const val NOTIFY_EVERYONE = "@room"
}
}
@ -172,6 +191,8 @@ fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun RoomSummary.toEveryoneInRoomMatrixItem() = MatrixItem.EveryoneInRoomItem(id = roomId, avatarUrl = avatarUrl, roomDisplayName = displayName)
// If no name is available, use room alias as Riot-Web does
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)

View File

@ -35,6 +35,8 @@ internal class CryptoSessionInfoProvider @Inject constructor(
) {
fun isRoomEncrypted(roomId: String): Boolean {
// We look at the presence at any m.room.encryption state event no matter if it's
// the latest one or if it is well formed
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.isEmpty(EventEntityFields.STATE_KEY)

View File

@ -240,6 +240,14 @@ internal interface IMXCryptoStore {
*/
fun getRoomAlgorithm(roomId: String): String?
/**
* This is a bit different than isRoomEncrypted
* A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not)
* But the crypto layer has additional guaranty to ensure that encryption would never been reverted
* It's defensive coding out of precaution (if ever state is reset)
*/
fun roomWasOnceEncrypted(roomId: String): Boolean
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)

View File

@ -631,7 +631,15 @@ internal class RealmCryptoStore @Inject constructor(
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
CryptoRoomEntity.getOrCreate(it, roomId).let { entity ->
entity.algorithm = algorithm
// store anyway the new algorithm, but mark the room
// as having been encrypted once whatever, this can never
// go back to false
if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
entity.wasEncryptedOnce = true
}
}
}
}
@ -641,6 +649,12 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun roomWasOnceEncrypted(roomId: String): Boolean {
return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false
}
}
override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
return doWithRealm(realmConfiguration) {
CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers

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

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,
// to avoid re-create and re-share at each startup (if rotation not needed..)
// This is specific to megolm but not sure how to model it better
var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null
var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null,
// a security to ensure that a room will never revert to not encrypted
// even if a new state event with empty encryption, or state is reset somehow
var wasEncryptedOnce: Boolean? = false
) :
RealmObject() {

View File

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

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

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

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

View File

@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills
import android.text.SpannableString
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
import java.util.Collections
import javax.inject.Inject
@ -51,6 +52,8 @@ internal class TextPillsUtils @Inject constructor(
val pills = spannableString
?.getSpans(0, text.length, MatrixItemSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
// we use the raw text for @room notification instead of a link
?.filterNot { it.span.matrixItem is MatrixItem.EveryoneInRoomItem }
?.toMutableList()
?.takeIf { it.isNotEmpty() }
?: return null

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

View File

@ -119,9 +119,8 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.roomType = roomType
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)

View File

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

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

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

View File

@ -34,6 +34,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.features.MainActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
@ -45,7 +46,6 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
@ -67,7 +67,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
@Before
fun createSessionWithCrossSigningAnd4S() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val matrix = Matrix.getInstance(context)
val matrix = getMatrixInstance()
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> {
@ -90,7 +90,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
runBlocking {
task.execute(Params(
userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(

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() {
override fun failed(e: Throwable?, description: Description) {
val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())}"
val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HHmmss").format(Date())}"
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
storeFailureScreenshot(bitmap, screenShotName)
}

View File

@ -16,10 +16,12 @@
package im.vector.app.ui
import android.Manifest
import androidx.test.espresso.IdlingPolicies
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.rule.GrantPermissionRule
import im.vector.app.R
import im.vector.app.espresso.tools.ScreenshotFailureRule
import im.vector.app.features.MainActivity
@ -43,6 +45,7 @@ class UiAllScreensSanityTest {
@get:Rule
val testRule = RuleChain
.outerRule(ActivityScenarioRule(MainActivity::class.java))
.around(GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE))
.around(ScreenshotFailureRule())
private val elementRobot = ElementRobot()
@ -94,6 +97,30 @@ class UiAllScreensSanityTest {
}
}
elementRobot.space {
createSpace {
crawl()
}
val spaceName = UUID.randomUUID().toString()
createSpace {
createPublicSpace(spaceName)
}
spaceMenu(spaceName) {
spaceMembers()
spaceSettings {
crawl()
}
exploreRooms()
invitePeople().also { openMenu(spaceName) }
addRoom().also { openMenu(spaceName) }
addSpace().also { openMenu(spaceName) }
leaveSpace()
}
}
elementRobot.withDeveloperMode {
settings {
advancedSettings { crawlDeveloperOptions() }

View File

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

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

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

View File

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

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

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

View File

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

View File

@ -16,19 +16,18 @@
package im.vector.app.features.analytics.impl
import android.content.Context
import com.posthog.android.Options
import com.posthog.android.PostHog
import com.posthog.android.Properties
import im.vector.app.BuildConfig
import im.vector.app.config.analyticsConfig
import im.vector.app.core.di.NamedGlobalScope
import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
import im.vector.app.features.analytics.log.analyticsTag
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.analytics.store.AnalyticsStore
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@ -41,15 +40,30 @@ private val IGNORED_OPTIONS: Options? = null
@Singleton
class DefaultVectorAnalytics @Inject constructor(
private val context: Context,
private val analyticsStore: AnalyticsStore
postHogFactory: PostHogFactory,
analyticsConfig: AnalyticsConfig,
private val analyticsStore: AnalyticsStore,
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@NamedGlobalScope private val globalScope: CoroutineScope
) : VectorAnalytics {
private var posthog: PostHog? = null
private val posthog: PostHog? = when {
analyticsConfig.isEnabled -> postHogFactory.createPosthog()
else -> {
Timber.tag(analyticsTag.value).w("Analytics is disabled")
null
}
}
// Cache for the store values
private var userConsent: Boolean? = null
private var analyticsId: String? = null
override fun init() {
observeUserConsent()
observeAnalyticsId()
}
override fun getUserConsent(): Flow<Boolean> {
return analyticsStore.userConsentFlow
}
@ -82,13 +96,6 @@ class DefaultVectorAnalytics @Inject constructor(
setAnalyticsId("")
}
override fun init() {
observeUserConsent()
observeAnalyticsId()
createAnalyticsClient()
}
@Suppress("EXPERIMENTAL_API_USAGE")
private fun observeAnalyticsId() {
getAnalyticsId()
.onEach { id ->
@ -96,21 +103,20 @@ class DefaultVectorAnalytics @Inject constructor(
analyticsId = id
identifyPostHog()
}
.launchIn(GlobalScope)
.launchIn(globalScope)
}
private fun identifyPostHog() {
private suspend fun identifyPostHog() {
val id = analyticsId ?: return
if (id.isEmpty()) {
Timber.tag(analyticsTag.value).d("reset")
posthog?.reset()
} else {
Timber.tag(analyticsTag.value).d("identify")
posthog?.identify(id)
posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS)
}
}
@Suppress("EXPERIMENTAL_API_USAGE")
private fun observeUserConsent() {
getUserConsent()
.onEach { consent ->
@ -118,49 +124,13 @@ class DefaultVectorAnalytics @Inject constructor(
userConsent = consent
optOutPostHog()
}
.launchIn(GlobalScope)
.launchIn(globalScope)
}
private fun optOutPostHog() {
userConsent?.let { posthog?.optOut(!it) }
}
private fun createAnalyticsClient() {
Timber.tag(analyticsTag.value).d("createAnalyticsClient()")
if (analyticsConfig.isEnabled.not()) {
Timber.tag(analyticsTag.value).w("Analytics is disabled")
return
}
posthog = PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
// Record certain application events automatically! (off/false by default)
// .captureApplicationLifecycleEvents()
// Record screen views automatically! (off/false by default)
// .recordScreenViews()
// Capture deep links as part of the screen call. (off by default)
// .captureDeepLinks()
// Maximum number of events to keep in queue before flushing (default 20)
// .flushQueueSize(20)
// Max delay before flushing the queue (30 seconds)
// .flushInterval(30, TimeUnit.SECONDS)
// Enable or disable collection of ANDROID_ID (true)
.collectDeviceId(false)
.logLevel(getLogLevel())
.build()
optOutPostHog()
identifyPostHog()
}
private fun getLogLevel(): PostHog.LogLevel {
return if (BuildConfig.DEBUG) {
PostHog.LogLevel.DEBUG
} else {
PostHog.LogLevel.INFO
}
}
override fun capture(event: VectorAnalyticsEvent) {
Timber.tag(analyticsTag.value).d("capture($event)")
posthog

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/
/**
* Triggered when the user changed screen
* Triggered when the user changed screen on Element Android/iOS
*/
data class Screen(
data class MobileScreen(
/**
* How long the screen was displayed for in milliseconds.
*/
@ -33,6 +33,11 @@ data class Screen(
) : VectorAnalyticsScreen {
enum class ScreenName {
/**
* The screen that displays the user's breadcrumbs.
*/
Breadcrumbs,
/**
* The screen shown to create a new (non-direct) room.
*/
@ -43,6 +48,16 @@ data class Screen(
*/
DeactivateAccount,
/**
* The tab on mobile that displays the dialpad.
*/
Dialpad,
/**
* The Favourites tab on mobile that lists your favourite people/rooms.
*/
Favourites,
/**
* The form for the forgot password use case
*/
@ -54,11 +69,15 @@ data class Screen(
Group,
/**
* The Home tab on iOS | possibly the same on Android? | Home page on
* Web
* The Home tab on iOS | possibly the same on Android?
*/
Home,
/**
* The screen shown to share a link to download the app.
*/
InviteFriends,
/**
* The screen that displays the login flow (when the user already has an
* account).
@ -66,100 +85,14 @@ data class Screen(
Login,
/**
* The screen that displays the user's breadcrumbs.
* Legacy: The screen that shows all groups/communities you have joined.
*/
MobileBreadcrumbs,
/**
* The tab on mobile that displays the dialpad.
*/
MobileDialpad,
/**
* The Favourites tab on mobile that lists your favourite people/rooms.
*/
MobileFavourites,
/**
* The screen shown to share a link to download the app.
*/
MobileInviteFriends,
MyGroups,
/**
* The People tab on mobile that lists all the DM rooms you have joined.
*/
MobilePeople,
/**
* The Rooms tab on mobile that lists all the (non-direct) rooms you've
* joined.
*/
MobileRooms,
/**
* The Files tab shown in the global search screen on Mobile.
*/
MobileSearchFiles,
/**
* The Messages tab shown in the global search screen on Mobile.
*/
MobileSearchMessages,
/**
* The People tab shown in the global search screen on Mobile.
*/
MobileSearchPeople,
/**
* The Rooms tab shown in the global search screen on Mobile.
*/
MobileSearchRooms,
/**
* The global settings screen shown in the app.
*/
MobileSettings,
/**
* The settings screen to change the default notification options.
*/
MobileSettingsDefaultNotifications,
/**
* The settings screen to manage notification mentions and keywords.
*/
MobileSettingsMentionsAndKeywords,
/**
* The global security settings screen.
*/
MobileSettingsSecurity,
/**
* The sidebar shown on mobile with spaces, settings etc.
*/
MobileSidebar,
/**
* Screen that displays the list of members of a space
*/
MobileSpaceMembers,
/**
* The bottom sheet that list all space options
*/
MobileSpaceMenu,
/**
* The screen shown to select which room directory you'd like to use.
*/
MobileSwitchDirectory,
/**
* Legacy: The screen that shows all groups/communities you have joined.
*/
MyGroups,
People,
/**
* The screen that displays the registration flow (when the user wants
@ -216,107 +149,87 @@ data class Screen(
*/
RoomUploads,
/**
* The Rooms tab on mobile that lists all the (non-direct) rooms you've
* joined.
*/
Rooms,
/**
* The Files tab shown in the global search screen on Mobile.
*/
SearchFiles,
/**
* The Messages tab shown in the global search screen on Mobile.
*/
SearchMessages,
/**
* The People tab shown in the global search screen on Mobile.
*/
SearchPeople,
/**
* The Rooms tab shown in the global search screen on Mobile.
*/
SearchRooms,
/**
* The global settings screen shown in the app.
*/
Settings,
/**
* The settings screen to change the default notification options.
*/
SettingsDefaultNotifications,
/**
* The settings screen to manage notification mentions and keywords.
*/
SettingsMentionsAndKeywords,
/**
* The global security settings screen.
*/
SettingsSecurity,
/**
* The sidebar shown on mobile with spaces, settings etc.
*/
Sidebar,
/**
* Screen that displays the list of rooms and spaces of a space
*/
SpaceExploreRooms,
/**
* Screen that displays the list of members of a space
*/
SpaceMembers,
/**
* The bottom sheet that list all space options
*/
SpaceMenu,
/**
* The screen shown to create a new direct room.
*/
StartChat,
/**
* The screen shown to select which room directory you'd like to use.
*/
SwitchDirectory,
/**
* A screen that shows information about a room member.
*/
User,
/**
* Element Web showing flow to trust this new device with cross-signing.
*/
WebCompleteSecurity,
/**
* Element Web showing flow to setup SSSS / cross-signing on this
* account.
*/
WebE2ESetup,
/**
* Element Web loading spinner.
*/
WebLoading,
/**
* Element Web device has been soft logged out by the server.
*/
WebSoftLogout,
/**
* Legacy: Element Web User Settings Flair Tab.
*/
WebUserSettingFlair,
/**
* Element Web User Settings Mjolnir (labs) Tab.
*/
WebUserSettingMjolnir,
/**
* Element Web User Settings Appearance Tab.
*/
WebUserSettingsAppearance,
/**
* Element Web User Settings General Tab.
*/
WebUserSettingsGeneral,
/**
* Element Web User Settings Help & About Tab.
*/
WebUserSettingsHelpAbout,
/**
* Element Web User Settings Ignored Users Tab.
*/
WebUserSettingsIgnoredUsers,
/**
* Element Web User Settings Keyboard Tab.
*/
WebUserSettingsKeyboard,
/**
* Element Web User Settings Labs Tab.
*/
WebUserSettingsLabs,
/**
* Element Web User Settings Notifications Tab.
*/
WebUserSettingsNotifications,
/**
* Element Web User Settings Preferences Tab.
*/
WebUserSettingsPreferences,
/**
* Element Web User Settings Security & Privacy Tab.
*/
WebUserSettingsSecurityPrivacy,
/**
* Element Web User Settings Sidebar Tab.
*/
WebUserSettingsSidebar,
/**
* Element Web User Settings Voice & Video Tab.
*/
WebUserSettingsVoiceVideo,
/**
* The splash screen.
*/

View File

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

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
import android.content.Context
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.autocompleteHeaderItem
import im.vector.app.features.autocomplete.autocompleteMatrixItem
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class AutocompleteMemberController @Inject constructor() : TypedEpoxyController<List<RoomMemberSummary>>() {
class AutocompleteMemberController @Inject constructor(private val context: Context) :
TypedEpoxyController<List<AutocompleteMemberItem>>() {
var listener: AutocompleteClickListener<RoomMemberSummary>? = null
/* ==========================================================================================
* Fields
* ========================================================================================== */
var listener: AutocompleteClickListener<AutocompleteMemberItem>? = null
/* ==========================================================================================
* Dependencies
* ========================================================================================== */
@Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<RoomMemberSummary>?) {
/* ==========================================================================================
* Specialization
* ========================================================================================== */
override fun buildModels(data: List<AutocompleteMemberItem>?) {
if (data.isNullOrEmpty()) {
return
}
data.forEach { item ->
when (item) {
is AutocompleteMemberItem.Header -> buildHeaderItem(item)
is AutocompleteMemberItem.RoomMember -> buildRoomMemberItem(item)
is AutocompleteMemberItem.Everyone -> buildEveryoneItem(item)
}
}
}
/* ==========================================================================================
* Helper methods
* ========================================================================================== */
private fun buildHeaderItem(header: AutocompleteMemberItem.Header) {
autocompleteHeaderItem {
id(header.id)
title(header.title)
}
}
private fun buildRoomMemberItem(roomMember: AutocompleteMemberItem.RoomMember) {
val host = this
data.forEach { user ->
autocompleteMatrixItem {
autocompleteMatrixItem {
roomMember.roomMemberSummary.let { user ->
id(user.userId)
matrixItem(user.toMatrixItem())
avatarRenderer(host.avatarRenderer)
clickListener { host.listener?.onItemClick(user) }
clickListener { host.listener?.onItemClick(roomMember) }
}
}
}
private fun buildEveryoneItem(everyone: AutocompleteMemberItem.Everyone) {
val host = this
autocompleteMatrixItem {
everyone.roomSummary.let { room ->
id(room.roomId)
matrixItem(room.toEveryoneInRoomMatrixItem())
subName(host.context.getString(R.string.room_message_notify_everyone))
avatarRenderer(host.avatarRenderer)
clickListener { host.listener?.onItemClick(everyone) }
}
}
}

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.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.features.autocomplete.AutocompleteClickListener
import im.vector.app.features.autocomplete.RecyclerViewPresenter
import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.util.MatrixItem
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
@Assisted val roomId: String,
session: Session,
private val session: Session,
private val controller: AutocompleteMemberController
) : RecyclerViewPresenter<RoomMemberSummary>(context), AutocompleteClickListener<RoomMemberSummary> {
) : RecyclerViewPresenter<AutocompleteMemberItem>(context), AutocompleteClickListener<AutocompleteMemberItem> {
/* ==========================================================================================
* Fields
* ========================================================================================== */
private val room by lazy { session.getRoom(roomId)!! }
/* ==========================================================================================
* Init
* ========================================================================================== */
init {
controller.listener = this
}
/* ==========================================================================================
* Public api
* ========================================================================================== */
fun clear() {
controller.listener = null
}
@ -50,29 +68,100 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
fun create(roomId: String): AutocompleteMemberPresenter
}
/* ==========================================================================================
* Specialization
* ========================================================================================== */
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return controller.adapter
}
override fun onItemClick(t: RoomMemberSummary) {
override fun onItemClick(t: AutocompleteMemberItem) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
val queryParams = roomMemberQueryParams {
displayName = if (query.isNullOrBlank()) {
QueryStringValue.IsNotEmpty
} else {
QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
val queryParams = createQueryParams(query)
val membersHeader = createMembersHeader()
val members = createMemberItems(queryParams)
val everyone = createEveryoneItem(query)
// add headers only when user can notify everyone
val canAddHeaders = canNotifyEveryone()
val items = mutableListOf<AutocompleteMemberItem>().apply {
if (members.isNotEmpty()) {
if (canAddHeaders) {
add(membersHeader)
}
addAll(members)
}
everyone?.let {
val everyoneHeader = createEveryoneHeader()
add(everyoneHeader)
add(it)
}
memberships = listOf(Membership.JOIN)
excludeSelf = true
}
val members = room.getRoomMembers(queryParams)
.asSequence()
.sortedBy { it.displayName }
.disambiguate()
controller.setData(members.toList())
controller.setData(items)
}
/* ==========================================================================================
* Helper methods
* ========================================================================================== */
private fun createQueryParams(query: CharSequence?) = roomMemberQueryParams {
displayName = if (query.isNullOrBlank()) {
QueryStringValue.IsNotEmpty
} else {
QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
}
memberships = listOf(Membership.JOIN)
excludeSelf = true
}
private fun createMembersHeader() =
AutocompleteMemberItem.Header(
ID_HEADER_MEMBERS,
context.getString(R.string.room_message_autocomplete_users)
)
private fun createMemberItems(queryParams: RoomMemberQueryParams) =
room.getRoomMembers(queryParams)
.asSequence()
.sortedBy { it.displayName }
.disambiguate()
.map { AutocompleteMemberItem.RoomMember(it) }
.toList()
private fun createEveryoneHeader() =
AutocompleteMemberItem.Header(
ID_HEADER_EVERYONE,
context.getString(R.string.room_message_autocomplete_notification)
)
private fun createEveryoneItem(query: CharSequence?) =
room.roomSummary()
?.takeIf { canNotifyEveryone() }
?.takeIf { query.isNullOrBlank() || MatrixItem.NOTIFY_EVERYONE.startsWith("@$query") }
?.let {
AutocompleteMemberItem.Everyone(it)
}
private fun canNotifyEveryone() = session.resolveSenderNotificationPermissionCondition(
Event(
senderId = session.myUserId,
roomId = roomId
),
SenderNotificationPermissionCondition(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY)
)
/* ==========================================================================================
* Const
* ========================================================================================== */
companion object {
private const val ID_HEADER_MEMBERS = "ID_HEADER_MEMBERS"
private const val ID_HEADER_EVERYONE = "ID_HEADER_EVERYONE"
}
}

View File

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

View File

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

View File

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

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.platform.VectorViewModel
import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.transfer.CallTransferResult
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem
import im.vector.app.features.createdirect.DirectRoomHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
@ -47,7 +51,9 @@ class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState,
val session: Session,
val callManager: WebRtcCallManager,
val proximityManager: CallProximityManager
val proximityManager: CallProximityManager,
private val dialPadLookup: DialPadLookup,
private val directRoomHelper: DirectRoomHelper,
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
private var call: WebRtcCall? = null
@ -327,6 +333,9 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewActions.CallTransferSelectionCancelled -> {
call?.updateRemoteOnHold(false)
}
is VectorCallViewActions.CallTransferSelectionResult -> {
handleCallTransferSelectionResult(action.callTransferResult)
}
VectorCallViewActions.TransferCall -> {
handleCallTransfer()
}
@ -345,6 +354,53 @@ class VectorCallViewModel @AssistedInject constructor(
}
}
private fun handleCallTransferSelectionResult(result: CallTransferResult) {
when (result) {
is CallTransferResult.ConnectWithUserId -> connectWithUserId(result)
is CallTransferResult.ConnectWithPhoneNumber -> connectWithPhoneNumber(result)
}.exhaustive
}
private fun connectWithUserId(result: CallTransferResult.ConnectWithUserId) {
viewModelScope.launch {
try {
if (result.consultFirst) {
val dmRoomId = directRoomHelper.ensureDMExists(result.selectedUserId)
callManager.startOutgoingCall(
nativeRoomId = dmRoomId,
otherUserId = result.selectedUserId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(result.selectedUserId, null)
}
} catch (failure: Throwable) {
_viewEvents.post(VectorCallViewEvents.FailToTransfer)
}
}
}
private fun connectWithPhoneNumber(action: CallTransferResult.ConnectWithPhoneNumber) {
viewModelScope.launch {
try {
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
if (action.consultFirst) {
callManager.startOutgoingCall(
nativeRoomId = result.roomId,
otherUserId = result.userId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(result.userId, result.roomId)
}
} catch (failure: Throwable) {
_viewEvents.post(VectorCallViewEvents.FailToTransfer)
}
}
}
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<VectorCallViewModel, VectorCallViewState> {
override fun create(initialState: VectorCallViewState): VectorCallViewModel

View File

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

View File

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

View File

@ -16,9 +16,10 @@
package im.vector.app.features.call.transfer
import im.vector.app.core.platform.VectorViewModelAction
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
sealed class CallTransferAction : VectorViewModelAction {
data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction()
sealed class CallTransferResult : Parcelable {
@Parcelize data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferResult()
@Parcelize data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferResult()
}

View File

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

View File

@ -22,22 +22,16 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
private val dialPadLookup: DialPadLookup,
private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager) :
VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
VectorViewModel<CallTransferViewState, EmptyAction, CallTransferViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<CallTransferViewModel, CallTransferViewState> {
@ -68,53 +62,5 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
call?.removeListener(callListener)
}
override fun handle(action: CallTransferAction) {
when (action) {
is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
}.exhaustive
}
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
viewModelScope.launch {
try {
if (action.consultFirst) {
val dmRoomId = directRoomHelper.ensureDMExists(action.selectedUserId)
callManager.startOutgoingCall(
nativeRoomId = dmRoomId,
otherUserId = action.selectedUserId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(action.selectedUserId, null)
}
_viewEvents.post(CallTransferViewEvents.Complete)
} catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
}
}
}
private fun connectWithPhoneNumber(action: CallTransferAction.ConnectWithPhoneNumber) {
viewModelScope.launch {
try {
_viewEvents.post(CallTransferViewEvents.Loading)
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
if (action.consultFirst) {
callManager.startOutgoingCall(
nativeRoomId = result.roomId,
otherUserId = result.userId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(result.userId, result.roomId)
}
_viewEvents.post(CallTransferViewEvents.Complete)
} catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
}
}
}
override fun handle(action: EmptyAction) { }
}

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

View File

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

View File

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

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.utils.startSharePlainTextIntent
import im.vector.app.databinding.FragmentHomeDrawerBinding
import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.spaces.SpaceListFragment
@ -98,7 +98,7 @@ class HomeDrawerFragment @Inject constructor(
views.homeDrawerInviteFriendButton.debouncedClicks {
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
analyticsTracker.screen(Screen(screenName = Screen.ScreenName.MobileInviteFriends))
analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends))
val text = getString(R.string.invite_friends_text, permalink)
startSharePlainTextIntent(

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

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

View File

@ -29,7 +29,7 @@ import java.io.File
* Transient events for RoomDetail
*/
sealed class RoomDetailViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
data class Failure(val throwable: Throwable, val showInDialog: Boolean = false) : RoomDetailViewEvents()
data class OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents()

View File

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

View File

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

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

View File

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

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.prevOrNull
import im.vector.app.features.home.AvatarRenderer
@ -26,10 +27,10 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisi
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedSimilarEventsItem
import im.vector.app.features.home.room.detail.timeline.item.MergedSimilarEventsItem_
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
@ -82,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
callback: TimelineEventController.Callback?): MergedSimilarEventsItem_? {
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
items,
currentPosition,
@ -122,23 +123,31 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedMembershipEventsItem.Attributes(
isCollapsed = isCollapsed,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
}
)
MergedMembershipEventsItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted)
.attributes(attributes)
.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
val summaryTitleResId = when (event.root.getClearType()) {
EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes
EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes
else -> null
}
summaryTitleResId?.let { summaryTitle ->
val attributes = MergedSimilarEventsItem.Attributes(
summaryTitleResId = summaryTitle,
isCollapsed = isCollapsed,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
}
)
MergedSimilarEventsItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted)
.attributes(attributes)
.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
}
}
}

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

View File

@ -56,7 +56,8 @@ object TimelineDisplayableEvents {
}
fun TimelineEvent.canBeMerged(): Boolean {
return root.getClearType() == EventType.STATE_ROOM_MEMBER
return root.getClearType() == EventType.STATE_ROOM_MEMBER ||
root.getClearType() == EventType.STATE_ROOM_SERVER_ACL
}
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {

View File

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

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

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

View File

@ -27,7 +27,7 @@ sealed class RoomListViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
data class Failure(val throwable: Throwable) : RoomListViewEvents()
data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents()
data class SelectRoom(val roomSummary: RoomSummary, val isInviteAlreadyAccepted: Boolean = false) : RoomListViewEvents()
object Done : RoomListViewEvents()
data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents()
}

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.resources.StringProvider
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.settings.VectorPreferences
@ -174,7 +173,7 @@ class RoomListViewModel @AssistedInject constructor(
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary, false))
}
private fun handleToggleSection(roomSection: RoomsSection) {
@ -208,6 +207,7 @@ class RoomListViewModel @AssistedInject constructor(
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary, true))
// quick echo
setState {
@ -221,18 +221,6 @@ class RoomListViewModel @AssistedInject constructor(
}
)
}
viewModelScope.launch {
try {
session.joinRoom(roomId)
analyticsTracker.capture(action.roomSummary.toAnalyticsJoinedRoom())
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined
} catch (failure: Throwable) {
// Notify the user
_viewEvents.post(RoomListViewEvents.Failure(failure))
}
}
}
private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->

View File

@ -19,6 +19,7 @@
package im.vector.app.features.html
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
@ -32,6 +33,7 @@ import im.vector.app.R
import im.vector.app.core.glide.GlideRequests
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.ref.WeakReference
@ -117,6 +119,11 @@ class PillImageSpan(private val glideRequests: GlideRequests,
setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = icon
if (matrixItem is MatrixItem.EveryoneInRoomItem) {
chipBackgroundColor = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.colorError))
// setTextColor API does not exist right now for ChipDrawable, use textAppearance
setTextAppearanceResource(R.style.TextAppearance_Vector_Body_OnError)
}
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
}

View File

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

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

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

View File

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

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