Merge branch 'develop' into feature/aris/thread_live_thread_list
# Conflicts: # matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
This commit is contained in:
commit
79a231f1dc
3
.github/workflows/sanity_test.yml
vendored
3
.github/workflows/sanity_test.yml
vendored
@ -69,9 +69,10 @@ jobs:
|
|||||||
touch emulator.log
|
touch emulator.log
|
||||||
chmod 777 emulator.log
|
chmod 777 emulator.log
|
||||||
adb logcat >> emulator.log &
|
adb logcat >> emulator.log &
|
||||||
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || adb pull storage/emulated/0/Pictures/failure_screenshots
|
./gradlew $CI_GRADLE_ARG_PROPERTIES -PallWarningsAsErrors=false connectedGplayDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=im.vector.app.ui.UiAllScreensSanityTest || (adb pull storage/emulated/0/Pictures/failure_screenshots && exit 1 )
|
||||||
- name: Upload Test Report Log
|
- name: Upload Test Report Log
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: sanity-error-results
|
name: sanity-error-results
|
||||||
path: |
|
path: |
|
||||||
|
1
.idea/dictionaries/bmarty.xml
generated
1
.idea/dictionaries/bmarty.xml
generated
@ -28,6 +28,7 @@
|
|||||||
<w>previewable</w>
|
<w>previewable</w>
|
||||||
<w>previewables</w>
|
<w>previewables</w>
|
||||||
<w>pstn</w>
|
<w>pstn</w>
|
||||||
|
<w>rageshake</w>
|
||||||
<w>riotx</w>
|
<w>riotx</w>
|
||||||
<w>signin</w>
|
<w>signin</w>
|
||||||
<w>signout</w>
|
<w>signout</w>
|
||||||
|
BIN
.idea/icon.png
generated
Normal file
BIN
.idea/icon.png
generated
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
1
changelog.d/2782.misc
Normal file
1
changelog.d/2782.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Collapse successive ACLs events in room timeline
|
1
changelog.d/3771.feature
Normal file
1
changelog.d/3771.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Open the room when user accepts an invite from the room list
|
1
changelog.d/4643.misc
Normal file
1
changelog.d/4643.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Home screen: Replacing search icon by filter icon in the top right menu
|
1
changelog.d/5104.misc
Normal file
1
changelog.d/5104.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Make Space creation screens more consistent
|
1
changelog.d/5123.feature
Normal file
1
changelog.d/5123.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add completion for @room to notify everyone in a room
|
1
changelog.d/5136.misc
Normal file
1
changelog.d/5136.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Defensive coding to ensure encryption when room was once e2e
|
1
changelog.d/5185.sdk
Normal file
1
changelog.d/5185.sdk
Normal file
@ -0,0 +1 @@
|
|||||||
|
Deprecates Matrix.initialize and Matrix.getInstance in favour of the client providing its own singleton instance via Matrix.createInstance
|
1
changelog.d/5201.bugfix
Normal file
1
changelog.d/5201.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix for call transfer with consult failing to make outgoing consultation call.
|
1
changelog.d/5225.misc
Normal file
1
changelog.d/5225.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Replacing color "vctr_unread_room_badge" by "vctr_content_secondary"
|
1
changelog.d/5234.bugfix
Normal file
1
changelog.d/5234.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Analytics: Fixes missing use case identity values from within the onboarding flow
|
1
changelog.d/5243.bugfix
Normal file
1
changelog.d/5243.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Increments database schema to take advantage of homeserver capabilities entity migration (fixes crash in pre-release builds)
|
1
changelog.d/5254.misc
Normal file
1
changelog.d/5254.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Change preferred jitsi domain from `jitsi.riot.im` to `meet.element.io`
|
1
changelog.d/5290.feature
Normal file
1
changelog.d/5290.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Support creating disclosed polls
|
@ -57,11 +57,6 @@
|
|||||||
<attr name="vctr_list_separator_on_surface" format="color" />
|
<attr name="vctr_list_separator_on_surface" format="color" />
|
||||||
|
|
||||||
<!-- Other colors, which are not in the palette -->
|
<!-- Other colors, which are not in the palette -->
|
||||||
<attr name="vctr_unread_room_badge" format="color" />
|
|
||||||
<color name="vctr_unread_room_badge_light">@color/palette_gray_200</color>
|
|
||||||
<color name="vctr_unread_room_badge_dark">@color/palette_gray_250</color>
|
|
||||||
<color name="vctr_unread_room_badge_black">@color/palette_gray_250</color>
|
|
||||||
|
|
||||||
<attr name="vctr_fab_label_bg" format="color" />
|
<attr name="vctr_fab_label_bg" format="color" />
|
||||||
<color name="vctr_fab_label_bg_light">@android:color/white</color>
|
<color name="vctr_fab_label_bg_light">@android:color/white</color>
|
||||||
<color name="vctr_fab_label_bg_dark">#FF181B21</color>
|
<color name="vctr_fab_label_bg_dark">#FF181B21</color>
|
||||||
|
@ -59,6 +59,10 @@
|
|||||||
<item name="android:fontFamily">sans-serif-medium</item>
|
<item name="android:fontFamily">sans-serif-medium</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.Vector.Body.OnError">
|
||||||
|
<item name="android:textColor">?colorOnError</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
|
<style name="TextAppearance.Vector.Caption" parent="TextAppearance.MaterialComponents.Caption">
|
||||||
<item name="fontFamily">sans-serif</item>
|
<item name="fontFamily">sans-serif</item>
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
<item name="android:fontFamily">sans-serif</item>
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
<!-- Only setting the items we need to override to get the background to be pure black, otherwise inheriting -->
|
<!-- Only setting the items we need to override to get the background to be pure black, otherwise inheriting -->
|
||||||
|
|
||||||
<!-- other colors -->
|
<!-- other colors -->
|
||||||
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_black</item>
|
|
||||||
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_black</item>
|
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_black</item>
|
||||||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_black</item>
|
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_black</item>
|
||||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>
|
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_black</item>
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
<item name="vctr_system">@color/element_system_dark</item>
|
<item name="vctr_system">@color/element_system_dark</item>
|
||||||
|
|
||||||
<!-- other colors -->
|
<!-- other colors -->
|
||||||
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_dark</item>
|
|
||||||
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_dark</item>
|
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_dark</item>
|
||||||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_dark</item>
|
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_dark</item>
|
||||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>
|
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_dark</item>
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
<item name="vctr_system">@color/element_system_light</item>
|
<item name="vctr_system">@color/element_system_light</item>
|
||||||
|
|
||||||
<!-- other colors -->
|
<!-- other colors -->
|
||||||
<item name="vctr_unread_room_badge">@color/vctr_unread_room_badge_light</item>
|
|
||||||
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_light</item>
|
<item name="vctr_fab_label_bg">@color/vctr_fab_label_bg_light</item>
|
||||||
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_light</item>
|
<item name="vctr_fab_label_stroke">@color/vctr_fab_label_stroke_light</item>
|
||||||
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>
|
<item name="vctr_fab_label_color">@color/vctr_fab_label_color_light</item>
|
||||||
|
@ -99,12 +99,31 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
|||||||
private lateinit var instance: Matrix
|
private lateinit var instance: Matrix
|
||||||
private val isInit = AtomicBoolean(false)
|
private val isInit = AtomicBoolean(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of Matrix, it's recommended to manage this instance as a singleton.
|
||||||
|
* To make use of the built in singleton use Matrix.initialize() and/or Matrix.getInstance(context) instead
|
||||||
|
**/
|
||||||
|
fun createInstance(context: Context, matrixConfiguration: MatrixConfiguration): Matrix {
|
||||||
|
return Matrix(context.applicationContext, matrixConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a singleton instance of Matrix for the given MatrixConfiguration
|
||||||
|
* This instance will be returned by Matrix.getInstance(context)
|
||||||
|
*/
|
||||||
|
@Deprecated("Use Matrix.createInstance and manage the instance manually")
|
||||||
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
|
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||||
if (isInit.compareAndSet(false, true)) {
|
if (isInit.compareAndSet(false, true)) {
|
||||||
instance = Matrix(context.applicationContext, matrixConfiguration)
|
instance = Matrix(context.applicationContext, matrixConfiguration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Either provides an already initialized singleton Matrix instance or queries the application context for a MatrixConfiguration.Provider
|
||||||
|
* to lazily create and store the instance.
|
||||||
|
*/
|
||||||
|
@Suppress("deprecation") // suppressing warning as this method is unused but is still provided for SDK clients
|
||||||
|
@Deprecated("Use Matrix.createInstance and manage the instance manually")
|
||||||
fun getInstance(context: Context): Matrix {
|
fun getInstance(context: Context): Matrix {
|
||||||
if (isInit.compareAndSet(false, true)) {
|
if (isInit.compareAndSet(false, true)) {
|
||||||
val appContext = context.applicationContext
|
val appContext = context.applicationContext
|
||||||
@ -113,7 +132,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
|||||||
instance = Matrix(appContext, matrixConfiguration)
|
instance = Matrix(appContext, matrixConfiguration)
|
||||||
} else {
|
} else {
|
||||||
throw IllegalStateException("Matrix is not initialized properly." +
|
throw IllegalStateException("Matrix is not initialized properly." +
|
||||||
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
|
" If you want to manage your own Matrix instance use Matrix.createInstance" +
|
||||||
|
" otherwise you should call Matrix.initialize or let your application implement MatrixConfiguration.Provider.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return instance
|
return instance
|
||||||
|
@ -66,6 +66,7 @@ data class MatrixConfiguration(
|
|||||||
/**
|
/**
|
||||||
* Can be implemented by your Application class.
|
* Can be implemented by your Application class.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use Matrix.createInstance and manage the instance manually instead of Matrix.getInstance")
|
||||||
interface Provider {
|
interface Provider {
|
||||||
fun providesMatrixConfiguration(): MatrixConfiguration
|
fun providesMatrixConfiguration(): MatrixConfiguration
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,9 @@ interface PushRuleService {
|
|||||||
|
|
||||||
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
// fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
|
||||||
|
|
||||||
|
fun resolveSenderNotificationPermissionCondition(event: Event,
|
||||||
|
condition: SenderNotificationPermissionCondition): Boolean
|
||||||
|
|
||||||
interface PushRuleListener {
|
interface PushRuleListener {
|
||||||
fun onEvents(pushEvents: PushEvents)
|
fun onEvents(pushEvents: PushEvents)
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,19 @@ sealed class MatrixItem(
|
|||||||
data class UserItem(override val id: String,
|
data class UserItem(override val id: String,
|
||||||
override val displayName: String? = null,
|
override val displayName: String? = null,
|
||||||
override val avatarUrl: String? = null) :
|
override val avatarUrl: String? = null) :
|
||||||
MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
|
MatrixItem(id, displayName?.removeSuffix(IRC_PATTERN), avatarUrl) {
|
||||||
|
init {
|
||||||
|
if (BuildConfig.DEBUG) checkId()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class EveryoneInRoomItem(override val id: String,
|
||||||
|
override val displayName: String = NOTIFY_EVERYONE,
|
||||||
|
override val avatarUrl: String? = null,
|
||||||
|
val roomDisplayName: String? = null) :
|
||||||
|
MatrixItem(id, displayName, avatarUrl) {
|
||||||
init {
|
init {
|
||||||
if (BuildConfig.DEBUG) checkId()
|
if (BuildConfig.DEBUG) checkId()
|
||||||
}
|
}
|
||||||
@ -47,7 +59,7 @@ sealed class MatrixItem(
|
|||||||
data class EventItem(override val id: String,
|
data class EventItem(override val id: String,
|
||||||
override val displayName: String? = null,
|
override val displayName: String? = null,
|
||||||
override val avatarUrl: String? = null) :
|
override val avatarUrl: String? = null) :
|
||||||
MatrixItem(id, displayName, avatarUrl) {
|
MatrixItem(id, displayName, avatarUrl) {
|
||||||
init {
|
init {
|
||||||
if (BuildConfig.DEBUG) checkId()
|
if (BuildConfig.DEBUG) checkId()
|
||||||
}
|
}
|
||||||
@ -58,7 +70,7 @@ sealed class MatrixItem(
|
|||||||
data class RoomItem(override val id: String,
|
data class RoomItem(override val id: String,
|
||||||
override val displayName: String? = null,
|
override val displayName: String? = null,
|
||||||
override val avatarUrl: String? = null) :
|
override val avatarUrl: String? = null) :
|
||||||
MatrixItem(id, displayName, avatarUrl) {
|
MatrixItem(id, displayName, avatarUrl) {
|
||||||
init {
|
init {
|
||||||
if (BuildConfig.DEBUG) checkId()
|
if (BuildConfig.DEBUG) checkId()
|
||||||
}
|
}
|
||||||
@ -69,7 +81,7 @@ sealed class MatrixItem(
|
|||||||
data class SpaceItem(override val id: String,
|
data class SpaceItem(override val id: String,
|
||||||
override val displayName: String? = null,
|
override val displayName: String? = null,
|
||||||
override val avatarUrl: String? = null) :
|
override val avatarUrl: String? = null) :
|
||||||
MatrixItem(id, displayName, avatarUrl) {
|
MatrixItem(id, displayName, avatarUrl) {
|
||||||
init {
|
init {
|
||||||
if (BuildConfig.DEBUG) checkId()
|
if (BuildConfig.DEBUG) checkId()
|
||||||
}
|
}
|
||||||
@ -80,7 +92,7 @@ sealed class MatrixItem(
|
|||||||
data class RoomAliasItem(override val id: String,
|
data class RoomAliasItem(override val id: String,
|
||||||
override val displayName: String? = null,
|
override val displayName: String? = null,
|
||||||
override val avatarUrl: String? = null) :
|
override val avatarUrl: String? = null) :
|
||||||
MatrixItem(id, displayName, avatarUrl) {
|
MatrixItem(id, displayName, avatarUrl) {
|
||||||
init {
|
init {
|
||||||
if (BuildConfig.DEBUG) checkId()
|
if (BuildConfig.DEBUG) checkId()
|
||||||
}
|
}
|
||||||
@ -91,7 +103,7 @@ sealed class MatrixItem(
|
|||||||
data class GroupItem(override val id: String,
|
data class GroupItem(override val id: String,
|
||||||
override val displayName: String? = null,
|
override val displayName: String? = null,
|
||||||
override val avatarUrl: String? = null) :
|
override val avatarUrl: String? = null) :
|
||||||
MatrixItem(id, displayName, avatarUrl) {
|
MatrixItem(id, displayName, avatarUrl) {
|
||||||
init {
|
init {
|
||||||
if (BuildConfig.DEBUG) checkId()
|
if (BuildConfig.DEBUG) checkId()
|
||||||
}
|
}
|
||||||
@ -110,16 +122,22 @@ sealed class MatrixItem(
|
|||||||
/**
|
/**
|
||||||
* Return the prefix as defined in the matrix spec (and not extracted from the id)
|
* Return the prefix as defined in the matrix spec (and not extracted from the id)
|
||||||
*/
|
*/
|
||||||
fun getIdPrefix() = when (this) {
|
private fun getIdPrefix() = when (this) {
|
||||||
is UserItem -> '@'
|
is UserItem -> '@'
|
||||||
is EventItem -> '$'
|
is EventItem -> '$'
|
||||||
is SpaceItem,
|
is SpaceItem,
|
||||||
is RoomItem -> '!'
|
is RoomItem,
|
||||||
is RoomAliasItem -> '#'
|
is EveryoneInRoomItem -> '!'
|
||||||
is GroupItem -> '+'
|
is RoomAliasItem -> '#'
|
||||||
|
is GroupItem -> '+'
|
||||||
}
|
}
|
||||||
|
|
||||||
fun firstLetterOfDisplayName(): String {
|
fun firstLetterOfDisplayName(): String {
|
||||||
|
val displayName = when (this) {
|
||||||
|
// use the room display name for the notify everyone item
|
||||||
|
is EveryoneInRoomItem -> roomDisplayName
|
||||||
|
else -> displayName
|
||||||
|
}
|
||||||
return (displayName?.takeIf { it.isNotBlank() } ?: id)
|
return (displayName?.takeIf { it.isNotBlank() } ?: id)
|
||||||
.let { dn ->
|
.let { dn ->
|
||||||
var startIndex = 0
|
var startIndex = 0
|
||||||
@ -152,7 +170,8 @@ sealed class MatrixItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ircPattern = " (IRC)"
|
private const val IRC_PATTERN = " (IRC)"
|
||||||
|
const val NOTIFY_EVERYONE = "@room"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +191,8 @@ fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
|
|||||||
|
|
||||||
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
|
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
|
||||||
|
|
||||||
|
fun RoomSummary.toEveryoneInRoomMatrixItem() = MatrixItem.EveryoneInRoomItem(id = roomId, avatarUrl = avatarUrl, roomDisplayName = displayName)
|
||||||
|
|
||||||
// If no name is available, use room alias as Riot-Web does
|
// If no name is available, use room alias as Riot-Web does
|
||||||
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
|
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
|
||||||
|
|
||||||
|
@ -35,6 +35,8 @@ internal class CryptoSessionInfoProvider @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun isRoomEncrypted(roomId: String): Boolean {
|
fun isRoomEncrypted(roomId: String): Boolean {
|
||||||
|
// We look at the presence at any m.room.encryption state event no matter if it's
|
||||||
|
// the latest one or if it is well formed
|
||||||
val encryptionEvent = monarchy.fetchCopied { realm ->
|
val encryptionEvent = monarchy.fetchCopied { realm ->
|
||||||
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||||
.isEmpty(EventEntityFields.STATE_KEY)
|
.isEmpty(EventEntityFields.STATE_KEY)
|
||||||
|
@ -240,6 +240,14 @@ internal interface IMXCryptoStore {
|
|||||||
*/
|
*/
|
||||||
fun getRoomAlgorithm(roomId: String): String?
|
fun getRoomAlgorithm(roomId: String): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a bit different than isRoomEncrypted
|
||||||
|
* A room is encrypted when there is a m.room.encryption state event in the room (malformed/invalid or not)
|
||||||
|
* But the crypto layer has additional guaranty to ensure that encryption would never been reverted
|
||||||
|
* It's defensive coding out of precaution (if ever state is reset)
|
||||||
|
*/
|
||||||
|
fun roomWasOnceEncrypted(roomId: String): Boolean
|
||||||
|
|
||||||
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
|
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
|
||||||
|
|
||||||
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
|
fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
|
||||||
|
@ -631,7 +631,15 @@ internal class RealmCryptoStore @Inject constructor(
|
|||||||
|
|
||||||
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
|
override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
|
CryptoRoomEntity.getOrCreate(it, roomId).let { entity ->
|
||||||
|
entity.algorithm = algorithm
|
||||||
|
// store anyway the new algorithm, but mark the room
|
||||||
|
// as having been encrypted once whatever, this can never
|
||||||
|
// go back to false
|
||||||
|
if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||||
|
entity.wasEncryptedOnce = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,6 +649,12 @@ internal class RealmCryptoStore @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun roomWasOnceEncrypted(roomId: String): Boolean {
|
||||||
|
return doWithRealm(realmConfiguration) {
|
||||||
|
CryptoRoomEntity.getById(it, roomId)?.wasEncryptedOnce ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
|
override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
|
||||||
return doWithRealm(realmConfiguration) {
|
return doWithRealm(realmConfiguration) {
|
||||||
CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers
|
CryptoRoomEntity.getById(it, roomId)?.shouldEncryptForInvitedMembers
|
||||||
|
@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
|
|||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo012
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo012
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo013
|
||||||
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration
|
|||||||
// 0, 1, 2: legacy Riot-Android
|
// 0, 1, 2: legacy Riot-Android
|
||||||
// 3: migrate to RiotX schema
|
// 3: migrate to RiotX schema
|
||||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||||
val schemaVersion = 14L
|
val schemaVersion = 15L
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
|
||||||
@ -65,5 +66,6 @@ internal class RealmCryptoStoreMigration @Inject constructor() : RealmMigration
|
|||||||
if (oldVersion < 12) MigrateCryptoTo012(realm).perform()
|
if (oldVersion < 12) MigrateCryptoTo012(realm).perform()
|
||||||
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
|
if (oldVersion < 13) MigrateCryptoTo013(realm).perform()
|
||||||
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
|
||||||
|
if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.crypto.store.db.migration
|
||||||
|
|
||||||
|
import io.realm.DynamicRealm
|
||||||
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||||
|
|
||||||
|
// Version 15L adds wasEncryptedOnce field to CryptoRoomEntity
|
||||||
|
class MigrateCryptoTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
|
||||||
|
|
||||||
|
override fun doMigrate(realm: DynamicRealm) {
|
||||||
|
realm.schema.get("CryptoRoomEntity")
|
||||||
|
?.addField(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, Boolean::class.java)
|
||||||
|
?.setNullable(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, true)
|
||||||
|
?.transform {
|
||||||
|
val currentAlgorithm = it.getString(CryptoRoomEntityFields.ALGORITHM)
|
||||||
|
it.set(CryptoRoomEntityFields.WAS_ENCRYPTED_ONCE, currentAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,10 @@ internal open class CryptoRoomEntity(
|
|||||||
// Store the current outbound session for this room,
|
// Store the current outbound session for this room,
|
||||||
// to avoid re-create and re-share at each startup (if rotation not needed..)
|
// to avoid re-create and re-share at each startup (if rotation not needed..)
|
||||||
// This is specific to megolm but not sure how to model it better
|
// This is specific to megolm but not sure how to model it better
|
||||||
var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null
|
var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null,
|
||||||
|
// a security to ensure that a room will never revert to not encrypted
|
||||||
|
// even if a new state event with empty encryption, or state is reset somehow
|
||||||
|
var wasEncryptedOnce: Boolean? = false
|
||||||
) :
|
) :
|
||||||
RealmObject() {
|
RealmObject() {
|
||||||
|
|
||||||
|
@ -19,11 +19,13 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import org.matrix.android.sdk.api.pushrules.Action
|
import org.matrix.android.sdk.api.pushrules.Action
|
||||||
|
import org.matrix.android.sdk.api.pushrules.ConditionResolver
|
||||||
import org.matrix.android.sdk.api.pushrules.PushEvents
|
import org.matrix.android.sdk.api.pushrules.PushEvents
|
||||||
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
import org.matrix.android.sdk.api.pushrules.PushRuleService
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleKind
|
import org.matrix.android.sdk.api.pushrules.RuleKind
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleScope
|
import org.matrix.android.sdk.api.pushrules.RuleScope
|
||||||
import org.matrix.android.sdk.api.pushrules.RuleSetKey
|
import org.matrix.android.sdk.api.pushrules.RuleSetKey
|
||||||
|
import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
|
||||||
import org.matrix.android.sdk.api.pushrules.getActions
|
import org.matrix.android.sdk.api.pushrules.getActions
|
||||||
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
import org.matrix.android.sdk.api.pushrules.rest.PushRule
|
||||||
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
|
import org.matrix.android.sdk.api.pushrules.rest.RuleSet
|
||||||
@ -53,6 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
|
|||||||
private val removePushRuleTask: RemovePushRuleTask,
|
private val removePushRuleTask: RemovePushRuleTask,
|
||||||
private val pushRuleFinder: PushRuleFinder,
|
private val pushRuleFinder: PushRuleFinder,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val conditionResolver: ConditionResolver,
|
||||||
@SessionDatabase private val monarchy: Monarchy
|
@SessionDatabase private val monarchy: Monarchy
|
||||||
) : PushRuleService {
|
) : PushRuleService {
|
||||||
|
|
||||||
@ -143,6 +146,10 @@ internal class DefaultPushRuleService @Inject constructor(
|
|||||||
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
|
return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun resolveSenderNotificationPermissionCondition(event: Event, condition: SenderNotificationPermissionCondition): Boolean {
|
||||||
|
return conditionResolver.resolveSenderNotificationPermissionCondition(event, condition)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getKeywords(): LiveData<Set<String>> {
|
override fun getKeywords(): LiveData<Set<String>> {
|
||||||
// Keywords are all content rules that don't start with '.'
|
// Keywords are all content rules that don't start with '.'
|
||||||
val liveData = monarchy.findAllMappedWithChanges(
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
|
@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.util.Cancelable
|
|||||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
|
||||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
@ -47,7 +46,6 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
private val eventEditor: EventEditor,
|
private val eventEditor: EventEditor,
|
||||||
private val eventSenderProcessor: EventSenderProcessor,
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
|
||||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||||
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
@ -144,7 +142,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
?.also { saveLocalEcho(it) }
|
?.also { saveLocalEcho(it) }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
|
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
|
||||||
@ -200,7 +198,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
|
|||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
|
||||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||||
@ -33,7 +32,6 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor,
|
internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor,
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
|
||||||
private val localEchoRepository: LocalEchoRepository) {
|
private val localEchoRepository: LocalEchoRepository) {
|
||||||
|
|
||||||
fun editTextMessage(targetEvent: TimelineEvent,
|
fun editTextMessage(targetEvent: TimelineEvent,
|
||||||
@ -51,7 +49,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||||||
} else if (targetEvent.root.sendState.isSent()) {
|
} else if (targetEvent.root.sendState.isSent()) {
|
||||||
val event = eventFactory
|
val event = eventFactory
|
||||||
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
||||||
return sendReplaceEvent(roomId, event)
|
return sendReplaceEvent(event)
|
||||||
} else {
|
} else {
|
||||||
// Should we throw?
|
// Should we throw?
|
||||||
Timber.w("Can't edit a sending event")
|
Timber.w("Can't edit a sending event")
|
||||||
@ -72,7 +70,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||||||
} else if (targetEvent.root.sendState.isSent()) {
|
} else if (targetEvent.root.sendState.isSent()) {
|
||||||
val event = eventFactory
|
val event = eventFactory
|
||||||
.createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options)
|
.createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options)
|
||||||
return sendReplaceEvent(roomId, event)
|
return sendReplaceEvent(event)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Can't edit a sending event")
|
Timber.w("Can't edit a sending event")
|
||||||
return NoOpCancellable
|
return NoOpCancellable
|
||||||
@ -82,12 +80,12 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||||||
private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable {
|
private fun sendFailedEvent(targetEvent: TimelineEvent, editedEvent: Event): Cancelable {
|
||||||
val roomId = targetEvent.roomId
|
val roomId = targetEvent.roomId
|
||||||
updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent)
|
updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent)
|
||||||
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(editedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendReplaceEvent(roomId: String, editedEvent: Event): Cancelable {
|
private fun sendReplaceEvent(editedEvent: Event): Cancelable {
|
||||||
localEchoRepository.createLocalEcho(editedEvent)
|
localEchoRepository.createLocalEcho(editedEvent)
|
||||||
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(editedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun editReply(replyToEdit: TimelineEvent,
|
fun editReply(replyToEdit: TimelineEvent,
|
||||||
@ -107,7 +105,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||||||
eventId = replyToEdit.eventId
|
eventId = replyToEdit.eventId
|
||||||
) ?: return NoOpCancellable
|
) ?: return NoOpCancellable
|
||||||
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
|
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
|
||||||
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(editedEvent)
|
||||||
} else if (replyToEdit.root.sendState.isSent()) {
|
} else if (replyToEdit.root.sendState.isSent()) {
|
||||||
val event = eventFactory.createReplaceTextOfReply(
|
val event = eventFactory.createReplaceTextOfReply(
|
||||||
roomId,
|
roomId,
|
||||||
@ -119,7 +117,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||||||
compatibilityBodyText
|
compatibilityBodyText
|
||||||
)
|
)
|
||||||
.also { localEchoRepository.createLocalEcho(it) }
|
.also { localEchoRepository.createLocalEcho(it) }
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event)
|
||||||
} else {
|
} else {
|
||||||
// Should we throw?
|
// Should we throw?
|
||||||
Timber.w("Can't edit a sending event")
|
Timber.w("Can't edit a sending event")
|
||||||
|
@ -46,7 +46,7 @@ import org.matrix.android.sdk.api.util.Cancelable
|
|||||||
import org.matrix.android.sdk.api.util.CancelableBag
|
import org.matrix.android.sdk.api.util.CancelableBag
|
||||||
import org.matrix.android.sdk.api.util.JsonDict
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
import org.matrix.android.sdk.api.util.NoOpCancellable
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.di.SessionId
|
import org.matrix.android.sdk.internal.di.SessionId
|
||||||
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
import org.matrix.android.sdk.internal.di.WorkManagerProvider
|
||||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||||
@ -66,7 +66,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
private val workManagerProvider: WorkManagerProvider,
|
private val workManagerProvider: WorkManagerProvider,
|
||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val localEchoRepository: LocalEchoRepository,
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
private val eventSenderProcessor: EventSenderProcessor,
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
@ -303,7 +303,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
|
private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
|
||||||
val cancelableBag = CancelableBag()
|
val cancelableBag = CancelableBag()
|
||||||
|
|
||||||
allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
|
allLocalEchoes.groupBy { cryptoStore.roomWasOnceEncrypted(it.roomId!!) }
|
||||||
.apply {
|
.apply {
|
||||||
keys.forEach { isRoomEncrypted ->
|
keys.forEach { isRoomEncrypted ->
|
||||||
// Should never be empty
|
// Should never be empty
|
||||||
@ -334,7 +334,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sendEvent(event: Event): Cancelable {
|
private fun sendEvent(event: Event): Cancelable {
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
|
return eventSenderProcessor.postEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLocalEcho(event: Event) {
|
private fun createLocalEcho(event: Event) {
|
||||||
|
@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.send.pills
|
|||||||
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
|
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -51,6 +52,8 @@ internal class TextPillsUtils @Inject constructor(
|
|||||||
val pills = spannableString
|
val pills = spannableString
|
||||||
?.getSpans(0, text.length, MatrixItemSpan::class.java)
|
?.getSpans(0, text.length, MatrixItemSpan::class.java)
|
||||||
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
|
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
|
||||||
|
// we use the raw text for @room notification instead of a link
|
||||||
|
?.filterNot { it.span.matrixItem is MatrixItem.EveryoneInRoomItem }
|
||||||
?.toMutableList()
|
?.toMutableList()
|
||||||
?.takeIf { it.isNotEmpty() }
|
?.takeIf { it.isNotEmpty() }
|
||||||
?: return null
|
?: return null
|
||||||
|
@ -26,9 +26,9 @@ import org.matrix.android.sdk.api.failure.Failure
|
|||||||
import org.matrix.android.sdk.api.failure.getRetryDelay
|
import org.matrix.android.sdk.api.failure.getRetryDelay
|
||||||
import org.matrix.android.sdk.api.failure.isLimitExceededError
|
import org.matrix.android.sdk.api.failure.isLimitExceededError
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
|
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import org.matrix.android.sdk.internal.task.CoroutineSequencer
|
import org.matrix.android.sdk.internal.task.CoroutineSequencer
|
||||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
||||||
@ -54,7 +54,7 @@ private const val MAX_RETRY_COUNT = 3
|
|||||||
*/
|
*/
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class EventSenderProcessorCoroutine @Inject constructor(
|
internal class EventSenderProcessorCoroutine @Inject constructor(
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val sessionParams: SessionParams,
|
private val sessionParams: SessionParams,
|
||||||
private val queuedTaskFactory: QueuedTaskFactory,
|
private val queuedTaskFactory: QueuedTaskFactory,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
@ -92,7 +92,8 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun postEvent(event: Event): Cancelable {
|
override fun postEvent(event: Event): Cancelable {
|
||||||
return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
|
val shouldEncrypt = event.roomId?.let { cryptoStore.roomWasOnceEncrypted(it) } ?: false
|
||||||
|
return postEvent(event, shouldEncrypt)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun postEvent(event: Event, encrypt: Boolean): Cancelable {
|
override fun postEvent(event: Event, encrypt: Boolean): Cancelable {
|
||||||
|
@ -119,9 +119,8 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
roomSummaryEntity.roomType = roomType
|
roomSummaryEntity.roomType = roomType
|
||||||
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
|
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
|
||||||
|
|
||||||
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
|
|
||||||
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
|
val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
|
||||||
Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
|
Timber.d("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
|
||||||
|
|
||||||
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
||||||
|
|
||||||
|
@ -192,12 +192,14 @@ abstract class SyncService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract fun provideMatrix(): Matrix
|
||||||
|
|
||||||
private fun initialize(intent: Intent?): Boolean {
|
private fun initialize(intent: Intent?): Boolean {
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
Timber.d("## Sync: initialize intent is null")
|
Timber.d("## Sync: initialize intent is null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val matrix = Matrix.getInstance(applicationContext)
|
val matrix = provideMatrix()
|
||||||
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
|
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
|
||||||
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds())
|
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds())
|
||||||
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds())
|
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds())
|
||||||
|
@ -38,7 +38,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
|
|||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import im.vector.app.core.utils.getMatrixInstance
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.crypto.recover.SetupMode
|
import im.vector.app.features.crypto.recover.SetupMode
|
||||||
import im.vector.app.features.home.HomeActivity
|
import im.vector.app.features.home.HomeActivity
|
||||||
@ -47,7 +47,6 @@ import org.junit.Before
|
|||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.matrix.android.sdk.api.Matrix
|
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@ -61,8 +60,7 @@ class SecurityBootstrapTest : VerificationTestBase() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun createSessionWithCrossSigning() {
|
fun createSessionWithCrossSigning() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val matrix = getMatrixInstance()
|
||||||
val matrix = Matrix.getInstance(context)
|
|
||||||
val userName = "foobar_${System.currentTimeMillis()}"
|
val userName = "foobar_${System.currentTimeMillis()}"
|
||||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||||
stubAllExternalIntents()
|
stubAllExternalIntents()
|
||||||
|
@ -33,7 +33,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
|
|||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import im.vector.app.core.utils.getMatrixInstance
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.home.HomeActivity
|
import im.vector.app.features.home.HomeActivity
|
||||||
import org.hamcrest.CoreMatchers.not
|
import org.hamcrest.CoreMatchers.not
|
||||||
@ -41,7 +41,6 @@ import org.junit.Before
|
|||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.matrix.android.sdk.api.Matrix
|
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
@ -66,8 +65,7 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun createSessionWithCrossSigning() {
|
fun createSessionWithCrossSigning() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val matrix = getMatrixInstance()
|
||||||
val matrix = Matrix.getInstance(context)
|
|
||||||
val userName = "foobar_${System.currentTimeMillis()}"
|
val userName = "foobar_${System.currentTimeMillis()}"
|
||||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||||
doSync<Unit> {
|
doSync<Unit> {
|
||||||
|
@ -34,6 +34,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.core.utils.getMatrixInstance
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||||
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
|
import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask
|
||||||
@ -45,7 +46,6 @@ import org.junit.Before
|
|||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.matrix.android.sdk.api.Matrix
|
|
||||||
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
import org.matrix.android.sdk.api.auth.UIABaseAuth
|
||||||
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
|
||||||
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
import org.matrix.android.sdk.api.auth.UserPasswordAuth
|
||||||
@ -67,7 +67,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
|
|||||||
@Before
|
@Before
|
||||||
fun createSessionWithCrossSigningAnd4S() {
|
fun createSessionWithCrossSigningAnd4S() {
|
||||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
val matrix = Matrix.getInstance(context)
|
val matrix = getMatrixInstance()
|
||||||
val userName = "foobar_${System.currentTimeMillis()}"
|
val userName = "foobar_${System.currentTimeMillis()}"
|
||||||
existingSession = createAccountAndSync(matrix, userName, password, true)
|
existingSession = createAccountAndSync(matrix, userName, password, true)
|
||||||
doSync<Unit> {
|
doSync<Unit> {
|
||||||
@ -90,7 +90,7 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
|
|||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
task.execute(Params(
|
task.execute(Params(
|
||||||
userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
|
userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
||||||
promise.resume(
|
promise.resume(
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.core.utils
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
|
|
||||||
|
fun getMatrixInstance(): Matrix {
|
||||||
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
val configuration = MatrixConfiguration(
|
||||||
|
roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(context)
|
||||||
|
)
|
||||||
|
return Matrix.createInstance(context, configuration)
|
||||||
|
}
|
@ -40,7 +40,7 @@ private val deviceLanguage = Locale.getDefault().language
|
|||||||
|
|
||||||
class ScreenshotFailureRule : TestWatcher() {
|
class ScreenshotFailureRule : TestWatcher() {
|
||||||
override fun failed(e: Throwable?, description: Description) {
|
override fun failed(e: Throwable?, description: Description) {
|
||||||
val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HH:mm:ss").format(Date())}"
|
val screenShotName = "$deviceLanguage-${description.methodName}-${SimpleDateFormat("EEE-MMMM-dd-HHmmss").format(Date())}"
|
||||||
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
|
val bitmap = getInstrumentation().uiAutomation.takeScreenshot()
|
||||||
storeFailureScreenshot(bitmap, screenShotName)
|
storeFailureScreenshot(bitmap, screenShotName)
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,12 @@
|
|||||||
|
|
||||||
package im.vector.app.ui
|
package im.vector.app.ui
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import androidx.test.espresso.IdlingPolicies
|
import androidx.test.espresso.IdlingPolicies
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.espresso.tools.ScreenshotFailureRule
|
import im.vector.app.espresso.tools.ScreenshotFailureRule
|
||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
@ -43,6 +45,7 @@ class UiAllScreensSanityTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val testRule = RuleChain
|
val testRule = RuleChain
|
||||||
.outerRule(ActivityScenarioRule(MainActivity::class.java))
|
.outerRule(ActivityScenarioRule(MainActivity::class.java))
|
||||||
|
.around(GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||||
.around(ScreenshotFailureRule())
|
.around(ScreenshotFailureRule())
|
||||||
|
|
||||||
private val elementRobot = ElementRobot()
|
private val elementRobot = ElementRobot()
|
||||||
@ -94,6 +97,30 @@ class UiAllScreensSanityTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elementRobot.space {
|
||||||
|
createSpace {
|
||||||
|
crawl()
|
||||||
|
}
|
||||||
|
val spaceName = UUID.randomUUID().toString()
|
||||||
|
createSpace {
|
||||||
|
createPublicSpace(spaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
spaceMenu(spaceName) {
|
||||||
|
spaceMembers()
|
||||||
|
spaceSettings {
|
||||||
|
crawl()
|
||||||
|
}
|
||||||
|
exploreRooms()
|
||||||
|
|
||||||
|
invitePeople().also { openMenu(spaceName) }
|
||||||
|
addRoom().also { openMenu(spaceName) }
|
||||||
|
addSpace().also { openMenu(spaceName) }
|
||||||
|
|
||||||
|
leaveSpace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
elementRobot.withDeveloperMode {
|
elementRobot.withDeveloperMode {
|
||||||
settings {
|
settings {
|
||||||
advancedSettings { crawlDeveloperOptions() }
|
advancedSettings { crawlDeveloperOptions() }
|
||||||
|
@ -35,6 +35,7 @@ import im.vector.app.features.home.HomeActivity
|
|||||||
import im.vector.app.features.onboarding.OnboardingActivity
|
import im.vector.app.features.onboarding.OnboardingActivity
|
||||||
import im.vector.app.initialSyncIdlingResource
|
import im.vector.app.initialSyncIdlingResource
|
||||||
import im.vector.app.ui.robot.settings.SettingsRobot
|
import im.vector.app.ui.robot.settings.SettingsRobot
|
||||||
|
import im.vector.app.ui.robot.space.SpaceRobot
|
||||||
import im.vector.app.withIdlingResource
|
import im.vector.app.withIdlingResource
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -147,6 +148,10 @@ class ElementRobot {
|
|||||||
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
|
waitUntilViewVisible(withId(R.id.bottomSheetFragmentContainer))
|
||||||
}.onFailure { Timber.w(it, "Verification popup missing") }
|
}.onFailure { Timber.w(it, "Verification popup missing") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun space(block: SpaceRobot.() -> Unit) {
|
||||||
|
block(SpaceRobot())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Boolean.toWarningType() = if (this) "shown" else "skipped"
|
private fun Boolean.toWarningType() = if (this) "shown" else "skipped"
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.ui.robot.space
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.Espresso.pressBack
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.espresso.tools.waitUntilActivityVisible
|
||||||
|
import im.vector.app.espresso.tools.waitUntilDialogVisible
|
||||||
|
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||||
|
import im.vector.app.features.home.HomeActivity
|
||||||
|
import im.vector.app.features.spaces.manage.SpaceManageActivity
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class SpaceCreateRobot {
|
||||||
|
|
||||||
|
fun crawl() {
|
||||||
|
// public
|
||||||
|
clickOn(R.id.publicButton)
|
||||||
|
waitUntilViewVisible(withId(R.id.recyclerView))
|
||||||
|
onView(ViewMatchers.withHint(R.string.create_room_name_hint)).perform(ViewActions.replaceText(UUID.randomUUID().toString()))
|
||||||
|
clickOn(R.id.nextButton)
|
||||||
|
waitUntilViewVisible(withId(R.id.recyclerView))
|
||||||
|
pressBack()
|
||||||
|
pressBack()
|
||||||
|
|
||||||
|
// private
|
||||||
|
clickOn(R.id.privateButton)
|
||||||
|
waitUntilViewVisible(withId(R.id.recyclerView))
|
||||||
|
clickOn(R.id.nextButton)
|
||||||
|
|
||||||
|
waitUntilViewVisible(withId(R.id.teammatesButton))
|
||||||
|
// me and teammates
|
||||||
|
clickOn(R.id.teammatesButton)
|
||||||
|
waitUntilViewVisible(withId(R.id.recyclerView))
|
||||||
|
clickOn(R.id.nextButton)
|
||||||
|
pressBack()
|
||||||
|
pressBack()
|
||||||
|
|
||||||
|
// just me
|
||||||
|
waitUntilViewVisible(withId(R.id.justMeButton))
|
||||||
|
clickOn(R.id.justMeButton)
|
||||||
|
waitUntilActivityVisible<SpaceManageActivity> {
|
||||||
|
waitUntilViewVisible(withId(R.id.roomList))
|
||||||
|
}
|
||||||
|
|
||||||
|
onView(withId(R.id.roomList))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
ViewMatchers.hasDescendant(withText(R.string.room_displayname_empty_room)),
|
||||||
|
click()
|
||||||
|
).atPosition(0)
|
||||||
|
)
|
||||||
|
clickOn(R.id.spaceAddRoomSaveItem)
|
||||||
|
waitUntilActivityVisible<HomeActivity> {
|
||||||
|
waitUntilViewVisible(withId(R.id.roomListContainer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPublicSpace(spaceName: String) {
|
||||||
|
clickOn(R.id.publicButton)
|
||||||
|
waitUntilViewVisible(withId(R.id.recyclerView))
|
||||||
|
onView(ViewMatchers.withHint(R.string.create_room_name_hint)).perform(ViewActions.replaceText(spaceName))
|
||||||
|
clickOn(R.id.nextButton)
|
||||||
|
waitUntilViewVisible(withId(R.id.recyclerView))
|
||||||
|
clickOn(R.id.nextButton)
|
||||||
|
waitUntilDialogVisible(withId(R.id.inviteByMxidButton))
|
||||||
|
// close invite dialog
|
||||||
|
pressBack()
|
||||||
|
waitUntilViewVisible(withId(R.id.timelineRecyclerView))
|
||||||
|
// close room
|
||||||
|
pressBack()
|
||||||
|
waitUntilViewVisible(withId(R.id.roomListContainer))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.ui.robot.space
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||||
|
import com.adevinta.android.barista.internal.viewaction.ClickChildAction
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.espresso.tools.waitUntilActivityVisible
|
||||||
|
import im.vector.app.espresso.tools.waitUntilDialogVisible
|
||||||
|
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||||
|
import im.vector.app.features.invite.InviteUsersToRoomActivity
|
||||||
|
import im.vector.app.features.roomprofile.RoomProfileActivity
|
||||||
|
import im.vector.app.features.spaces.SpaceExploreActivity
|
||||||
|
import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
|
||||||
|
import im.vector.app.features.spaces.manage.SpaceManageActivity
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
|
||||||
|
class SpaceMenuRobot {
|
||||||
|
|
||||||
|
fun openMenu(spaceName: String) {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
|
||||||
|
onView(ViewMatchers.withId(R.id.groupListView))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
ViewMatchers.hasDescendant(Matchers.allOf(ViewMatchers.withId(R.id.groupNameView), ViewMatchers.withText(spaceName))),
|
||||||
|
ClickChildAction.clickChildWithId(R.id.groupTmpLeave)
|
||||||
|
).atPosition(0)
|
||||||
|
)
|
||||||
|
waitUntilDialogVisible(ViewMatchers.withId(R.id.spaceNameView))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invitePeople() = apply {
|
||||||
|
clickOn(R.id.invitePeople)
|
||||||
|
waitUntilDialogVisible(ViewMatchers.withId(R.id.inviteByMxidButton))
|
||||||
|
clickOn(R.id.inviteByMxidButton)
|
||||||
|
waitUntilActivityVisible<InviteUsersToRoomActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.userListRecyclerView))
|
||||||
|
}
|
||||||
|
// close keyboard
|
||||||
|
Espresso.pressBack()
|
||||||
|
// close invite view
|
||||||
|
Espresso.pressBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun spaceMembers() {
|
||||||
|
clickOn(R.id.showMemberList)
|
||||||
|
waitUntilActivityVisible<RoomProfileActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||||
|
}
|
||||||
|
Espresso.pressBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun spaceSettings(block: SpaceSettingsRobot.() -> Unit) {
|
||||||
|
clickOn(R.id.spaceSettings)
|
||||||
|
waitUntilActivityVisible<SpaceManageActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||||
|
}
|
||||||
|
block(SpaceSettingsRobot())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exploreRooms() {
|
||||||
|
clickOn(R.id.exploreRooms)
|
||||||
|
waitUntilActivityVisible<SpaceExploreActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.spaceDirectoryList))
|
||||||
|
}
|
||||||
|
Espresso.pressBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addRoom() = apply {
|
||||||
|
clickOn(R.id.addRooms)
|
||||||
|
waitUntilActivityVisible<SpaceManageActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||||
|
}
|
||||||
|
Espresso.pressBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addSpace() = apply {
|
||||||
|
clickOn(R.id.addSpaces)
|
||||||
|
waitUntilActivityVisible<SpaceManageActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||||
|
}
|
||||||
|
Espresso.pressBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun leaveSpace() {
|
||||||
|
clickOn(R.id.leaveSpace)
|
||||||
|
waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton))
|
||||||
|
clickOn(R.id.leave_selected)
|
||||||
|
waitUntilActivityVisible<SpaceLeaveAdvancedActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||||
|
}
|
||||||
|
clickOn(R.id.spaceLeaveButton)
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.ui.robot.space
|
||||||
|
|
||||||
|
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||||
|
import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawer
|
||||||
|
import im.vector.app.R
|
||||||
|
|
||||||
|
class SpaceRobot {
|
||||||
|
|
||||||
|
fun createSpace(block: SpaceCreateRobot.() -> Unit) {
|
||||||
|
openDrawer()
|
||||||
|
clickOn(R.string.add_space)
|
||||||
|
block(SpaceCreateRobot())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun spaceMenu(spaceName: String, block: SpaceMenuRobot.() -> Unit) {
|
||||||
|
openDrawer()
|
||||||
|
with(SpaceMenuRobot()) {
|
||||||
|
openMenu(spaceName)
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.ui.robot.space
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.espresso.tools.waitUntilActivityVisible
|
||||||
|
import im.vector.app.espresso.tools.waitUntilViewVisible
|
||||||
|
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleActivity
|
||||||
|
|
||||||
|
class SpaceSettingsRobot {
|
||||||
|
fun crawl() {
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.room_settings_space_access_title)),
|
||||||
|
ViewActions.click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
waitUntilActivityVisible<RoomJoinRuleActivity> {
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.genericRecyclerView))
|
||||||
|
}
|
||||||
|
|
||||||
|
Espresso.pressBack()
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.space_settings_manage_rooms)),
|
||||||
|
ViewActions.click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomList))
|
||||||
|
Espresso.pressBack()
|
||||||
|
|
||||||
|
Espresso.onView(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||||
|
.perform(
|
||||||
|
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||||
|
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.space_settings_permissions_title)),
|
||||||
|
ViewActions.click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
waitUntilViewVisible(ViewMatchers.withId(R.id.roomSettingsRecyclerView))
|
||||||
|
Espresso.pressBack()
|
||||||
|
Espresso.pressBack()
|
||||||
|
}
|
||||||
|
}
|
@ -55,7 +55,6 @@ import im.vector.app.features.pin.PinLocker
|
|||||||
import im.vector.app.features.popup.PopupAlertManager
|
import im.vector.app.features.popup.PopupAlertManager
|
||||||
import im.vector.app.features.rageshake.VectorFileLogger
|
import im.vector.app.features.rageshake.VectorFileLogger
|
||||||
import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler
|
import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler
|
||||||
import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
|
|
||||||
import im.vector.app.features.settings.VectorLocale
|
import im.vector.app.features.settings.VectorLocale
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
@ -63,7 +62,6 @@ import im.vector.app.features.version.VersionProvider
|
|||||||
import im.vector.app.push.fcm.FcmHelper
|
import im.vector.app.push.fcm.FcmHelper
|
||||||
import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler
|
import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler
|
||||||
import org.matrix.android.sdk.api.Matrix
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
|
||||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
|
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -77,7 +75,6 @@ import androidx.work.Configuration as WorkConfiguration
|
|||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class VectorApplication :
|
class VectorApplication :
|
||||||
Application(),
|
Application(),
|
||||||
MatrixConfiguration.Provider,
|
|
||||||
WorkConfiguration.Provider {
|
WorkConfiguration.Provider {
|
||||||
|
|
||||||
lateinit var appContext: Context
|
lateinit var appContext: Context
|
||||||
@ -100,6 +97,7 @@ class VectorApplication :
|
|||||||
@Inject lateinit var autoRageShaker: AutoRageShaker
|
@Inject lateinit var autoRageShaker: AutoRageShaker
|
||||||
@Inject lateinit var vectorFileLogger: VectorFileLogger
|
@Inject lateinit var vectorFileLogger: VectorFileLogger
|
||||||
@Inject lateinit var vectorAnalytics: VectorAnalytics
|
@Inject lateinit var vectorAnalytics: VectorAnalytics
|
||||||
|
@Inject lateinit var matrix: Matrix
|
||||||
|
|
||||||
// font thread handler
|
// font thread handler
|
||||||
private var fontThreadHandler: Handler? = null
|
private var fontThreadHandler: Handler? = null
|
||||||
@ -220,16 +218,9 @@ class VectorApplication :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun providesMatrixConfiguration(): MatrixConfiguration {
|
|
||||||
return MatrixConfiguration(
|
|
||||||
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
|
|
||||||
roomDisplayNameFallbackProvider = VectorRoomDisplayNameFallbackProvider(this)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): WorkConfiguration {
|
override fun getWorkManagerConfiguration(): WorkConfiguration {
|
||||||
return WorkConfiguration.Builder()
|
return WorkConfiguration.Builder()
|
||||||
.setWorkerFactory(Matrix.getInstance(this.appContext).workerFactory())
|
.setWorkerFactory(matrix.workerFactory())
|
||||||
.setExecutor(Executors.newCachedThreadPool())
|
.setExecutor(Executors.newCachedThreadPool())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.core.di
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class NamedGlobalScope
|
@ -26,13 +26,16 @@ import dagger.Module
|
|||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.EmojiCompatWrapper
|
import im.vector.app.EmojiCompatWrapper
|
||||||
import im.vector.app.EmojiSpanify
|
import im.vector.app.EmojiSpanify
|
||||||
|
import im.vector.app.config.analyticsConfig
|
||||||
import im.vector.app.core.dispatchers.CoroutineDispatchers
|
import im.vector.app.core.dispatchers.CoroutineDispatchers
|
||||||
import im.vector.app.core.error.DefaultErrorFormatter
|
import im.vector.app.core.error.DefaultErrorFormatter
|
||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
import im.vector.app.core.time.Clock
|
import im.vector.app.core.time.Clock
|
||||||
import im.vector.app.core.time.DefaultClock
|
import im.vector.app.core.time.DefaultClock
|
||||||
|
import im.vector.app.features.analytics.AnalyticsConfig
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.VectorAnalytics
|
import im.vector.app.features.analytics.VectorAnalytics
|
||||||
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
|
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
|
||||||
@ -42,12 +45,15 @@ import im.vector.app.features.navigation.DefaultNavigator
|
|||||||
import im.vector.app.features.navigation.Navigator
|
import im.vector.app.features.navigation.Navigator
|
||||||
import im.vector.app.features.pin.PinCodeStore
|
import im.vector.app.features.pin.PinCodeStore
|
||||||
import im.vector.app.features.pin.SharedPrefPinCodeStore
|
import im.vector.app.features.pin.SharedPrefPinCodeStore
|
||||||
|
import im.vector.app.features.room.VectorRoomDisplayNameFallbackProvider
|
||||||
import im.vector.app.features.ui.SharedPreferencesUiStateRepository
|
import im.vector.app.features.ui.SharedPreferencesUiStateRepository
|
||||||
import im.vector.app.features.ui.UiStateRepository
|
import im.vector.app.features.ui.UiStateRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import org.matrix.android.sdk.api.Matrix
|
import org.matrix.android.sdk.api.Matrix
|
||||||
|
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
||||||
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
|
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
|
||||||
@ -107,8 +113,17 @@ object VectorStaticModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesMatrix(context: Context): Matrix {
|
fun providesMatrixConfiguration(vectorRoomDisplayNameFallbackProvider: VectorRoomDisplayNameFallbackProvider): MatrixConfiguration {
|
||||||
return Matrix.getInstance(context)
|
return MatrixConfiguration(
|
||||||
|
applicationFlavor = BuildConfig.FLAVOR_DESCRIPTION,
|
||||||
|
roomDisplayNameFallbackProvider = vectorRoomDisplayNameFallbackProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesMatrix(context: Context, configuration: MatrixConfiguration): Matrix {
|
||||||
|
return Matrix.createInstance(context, configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ -147,4 +162,16 @@ object VectorStaticModule {
|
|||||||
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
fun providesCoroutineDispatchers(): CoroutineDispatchers {
|
||||||
return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default)
|
return CoroutineDispatchers(io = Dispatchers.IO, computation = Dispatchers.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
|
@Provides
|
||||||
|
@NamedGlobalScope
|
||||||
|
fun providesGlobalScope(): CoroutineScope {
|
||||||
|
return GlobalScope
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesAnalyticsConfig(): AnalyticsConfig {
|
||||||
|
return analyticsConfig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ import im.vector.app.core.utils.toast
|
|||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.MainActivityArgs
|
import im.vector.app.features.MainActivityArgs
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.analytics.screen.ScreenEvent
|
import im.vector.app.features.analytics.screen.ScreenEvent
|
||||||
import im.vector.app.features.configuration.VectorConfiguration
|
import im.vector.app.features.configuration.VectorConfiguration
|
||||||
import im.vector.app.features.consent.ConsentNotGivenHelper
|
import im.vector.app.features.consent.ConsentNotGivenHelper
|
||||||
@ -97,7 +97,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
|
|||||||
* Analytics
|
* Analytics
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
protected var analyticsScreenName: Screen.ScreenName? = null
|
protected var analyticsScreenName: MobileScreen.ScreenName? = null
|
||||||
private var screenEvent: ScreenEvent? = null
|
private var screenEvent: ScreenEvent? = null
|
||||||
|
|
||||||
protected lateinit var analyticsTracker: AnalyticsTracker
|
protected lateinit var analyticsTracker: AnalyticsTracker
|
||||||
|
@ -38,7 +38,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
|
|||||||
import im.vector.app.core.extensions.toMvRxBundle
|
import im.vector.app.core.extensions.toMvRxBundle
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.analytics.screen.ScreenEvent
|
import im.vector.app.features.analytics.screen.ScreenEvent
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -53,7 +53,7 @@ abstract class VectorBaseBottomSheetDialogFragment<VB : ViewBinding> : BottomShe
|
|||||||
* Analytics
|
* Analytics
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
protected var analyticsScreenName: Screen.ScreenName? = null
|
protected var analyticsScreenName: MobileScreen.ScreenName? = null
|
||||||
private var screenEvent: ScreenEvent? = null
|
private var screenEvent: ScreenEvent? = null
|
||||||
|
|
||||||
protected lateinit var analyticsTracker: AnalyticsTracker
|
protected lateinit var analyticsTracker: AnalyticsTracker
|
||||||
|
@ -44,7 +44,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
|
|||||||
import im.vector.app.core.extensions.toMvRxBundle
|
import im.vector.app.core.extensions.toMvRxBundle
|
||||||
import im.vector.app.core.utils.ToolbarConfig
|
import im.vector.app.core.utils.ToolbarConfig
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.analytics.screen.ScreenEvent
|
import im.vector.app.features.analytics.screen.ScreenEvent
|
||||||
import im.vector.app.features.navigation.Navigator
|
import im.vector.app.features.navigation.Navigator
|
||||||
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
|
import im.vector.lib.ui.styles.dialogs.MaterialProgressDialog
|
||||||
@ -58,7 +58,7 @@ abstract class VectorBaseFragment<VB : ViewBinding> : Fragment(), MavericksView
|
|||||||
* Analytics
|
* Analytics
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
protected var analyticsScreenName: Screen.ScreenName? = null
|
protected var analyticsScreenName: MobileScreen.ScreenName? = null
|
||||||
private var screenEvent: ScreenEvent? = null
|
private var screenEvent: ScreenEvent? = null
|
||||||
|
|
||||||
protected lateinit var analyticsTracker: AnalyticsTracker
|
protected lateinit var analyticsTracker: AnalyticsTracker
|
||||||
|
@ -35,6 +35,7 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.platform.PendingIntentCompat
|
import im.vector.app.core.platform.PendingIntentCompat
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
import im.vector.app.features.settings.BackgroundSyncMode
|
import im.vector.app.features.settings.BackgroundSyncMode
|
||||||
|
import org.matrix.android.sdk.api.Matrix
|
||||||
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
import org.matrix.android.sdk.internal.session.sync.job.SyncService
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -75,6 +76,9 @@ class VectorSyncService : SyncService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Inject lateinit var notificationUtils: NotificationUtils
|
@Inject lateinit var notificationUtils: NotificationUtils
|
||||||
|
@Inject lateinit var matrix: Matrix
|
||||||
|
|
||||||
|
override fun provideMatrix() = matrix
|
||||||
|
|
||||||
override fun getDefaultSyncDelaySeconds() = BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS
|
override fun getDefaultSyncDelaySeconds() = BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS
|
||||||
|
|
||||||
|
@ -16,19 +16,18 @@
|
|||||||
|
|
||||||
package im.vector.app.features.analytics.impl
|
package im.vector.app.features.analytics.impl
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.posthog.android.Options
|
import com.posthog.android.Options
|
||||||
import com.posthog.android.PostHog
|
import com.posthog.android.PostHog
|
||||||
import com.posthog.android.Properties
|
import com.posthog.android.Properties
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.core.di.NamedGlobalScope
|
||||||
import im.vector.app.config.analyticsConfig
|
import im.vector.app.features.analytics.AnalyticsConfig
|
||||||
import im.vector.app.features.analytics.VectorAnalytics
|
import im.vector.app.features.analytics.VectorAnalytics
|
||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
import im.vector.app.features.analytics.itf.VectorAnalyticsEvent
|
||||||
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
||||||
import im.vector.app.features.analytics.log.analyticsTag
|
import im.vector.app.features.analytics.log.analyticsTag
|
||||||
import im.vector.app.features.analytics.plan.UserProperties
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import im.vector.app.features.analytics.store.AnalyticsStore
|
import im.vector.app.features.analytics.store.AnalyticsStore
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -41,15 +40,30 @@ private val IGNORED_OPTIONS: Options? = null
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class DefaultVectorAnalytics @Inject constructor(
|
class DefaultVectorAnalytics @Inject constructor(
|
||||||
private val context: Context,
|
postHogFactory: PostHogFactory,
|
||||||
private val analyticsStore: AnalyticsStore
|
analyticsConfig: AnalyticsConfig,
|
||||||
|
private val analyticsStore: AnalyticsStore,
|
||||||
|
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
|
||||||
|
@NamedGlobalScope private val globalScope: CoroutineScope
|
||||||
) : VectorAnalytics {
|
) : VectorAnalytics {
|
||||||
private var posthog: PostHog? = null
|
|
||||||
|
private val posthog: PostHog? = when {
|
||||||
|
analyticsConfig.isEnabled -> postHogFactory.createPosthog()
|
||||||
|
else -> {
|
||||||
|
Timber.tag(analyticsTag.value).w("Analytics is disabled")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cache for the store values
|
// Cache for the store values
|
||||||
private var userConsent: Boolean? = null
|
private var userConsent: Boolean? = null
|
||||||
private var analyticsId: String? = null
|
private var analyticsId: String? = null
|
||||||
|
|
||||||
|
override fun init() {
|
||||||
|
observeUserConsent()
|
||||||
|
observeAnalyticsId()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getUserConsent(): Flow<Boolean> {
|
override fun getUserConsent(): Flow<Boolean> {
|
||||||
return analyticsStore.userConsentFlow
|
return analyticsStore.userConsentFlow
|
||||||
}
|
}
|
||||||
@ -82,13 +96,6 @@ class DefaultVectorAnalytics @Inject constructor(
|
|||||||
setAnalyticsId("")
|
setAnalyticsId("")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun init() {
|
|
||||||
observeUserConsent()
|
|
||||||
observeAnalyticsId()
|
|
||||||
createAnalyticsClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
|
||||||
private fun observeAnalyticsId() {
|
private fun observeAnalyticsId() {
|
||||||
getAnalyticsId()
|
getAnalyticsId()
|
||||||
.onEach { id ->
|
.onEach { id ->
|
||||||
@ -96,21 +103,20 @@ class DefaultVectorAnalytics @Inject constructor(
|
|||||||
analyticsId = id
|
analyticsId = id
|
||||||
identifyPostHog()
|
identifyPostHog()
|
||||||
}
|
}
|
||||||
.launchIn(GlobalScope)
|
.launchIn(globalScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun identifyPostHog() {
|
private suspend fun identifyPostHog() {
|
||||||
val id = analyticsId ?: return
|
val id = analyticsId ?: return
|
||||||
if (id.isEmpty()) {
|
if (id.isEmpty()) {
|
||||||
Timber.tag(analyticsTag.value).d("reset")
|
Timber.tag(analyticsTag.value).d("reset")
|
||||||
posthog?.reset()
|
posthog?.reset()
|
||||||
} else {
|
} else {
|
||||||
Timber.tag(analyticsTag.value).d("identify")
|
Timber.tag(analyticsTag.value).d("identify")
|
||||||
posthog?.identify(id)
|
posthog?.identify(id, lateInitUserPropertiesFactory.createUserProperties()?.getProperties()?.toPostHogUserProperties(), IGNORED_OPTIONS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
|
||||||
private fun observeUserConsent() {
|
private fun observeUserConsent() {
|
||||||
getUserConsent()
|
getUserConsent()
|
||||||
.onEach { consent ->
|
.onEach { consent ->
|
||||||
@ -118,49 +124,13 @@ class DefaultVectorAnalytics @Inject constructor(
|
|||||||
userConsent = consent
|
userConsent = consent
|
||||||
optOutPostHog()
|
optOutPostHog()
|
||||||
}
|
}
|
||||||
.launchIn(GlobalScope)
|
.launchIn(globalScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun optOutPostHog() {
|
private fun optOutPostHog() {
|
||||||
userConsent?.let { posthog?.optOut(!it) }
|
userConsent?.let { posthog?.optOut(!it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAnalyticsClient() {
|
|
||||||
Timber.tag(analyticsTag.value).d("createAnalyticsClient()")
|
|
||||||
|
|
||||||
if (analyticsConfig.isEnabled.not()) {
|
|
||||||
Timber.tag(analyticsTag.value).w("Analytics is disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
posthog = PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
|
|
||||||
// Record certain application events automatically! (off/false by default)
|
|
||||||
// .captureApplicationLifecycleEvents()
|
|
||||||
// Record screen views automatically! (off/false by default)
|
|
||||||
// .recordScreenViews()
|
|
||||||
// Capture deep links as part of the screen call. (off by default)
|
|
||||||
// .captureDeepLinks()
|
|
||||||
// Maximum number of events to keep in queue before flushing (default 20)
|
|
||||||
// .flushQueueSize(20)
|
|
||||||
// Max delay before flushing the queue (30 seconds)
|
|
||||||
// .flushInterval(30, TimeUnit.SECONDS)
|
|
||||||
// Enable or disable collection of ANDROID_ID (true)
|
|
||||||
.collectDeviceId(false)
|
|
||||||
.logLevel(getLogLevel())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
optOutPostHog()
|
|
||||||
identifyPostHog()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLogLevel(): PostHog.LogLevel {
|
|
||||||
return if (BuildConfig.DEBUG) {
|
|
||||||
PostHog.LogLevel.DEBUG
|
|
||||||
} else {
|
|
||||||
PostHog.LogLevel.INFO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun capture(event: VectorAnalyticsEvent) {
|
override fun capture(event: VectorAnalyticsEvent) {
|
||||||
Timber.tag(analyticsTag.value).d("capture($event)")
|
Timber.tag(analyticsTag.value).d("capture($event)")
|
||||||
posthog
|
posthog
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.analytics.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import im.vector.app.ActiveSessionDataSource
|
||||||
|
import im.vector.app.core.extensions.vectorStore
|
||||||
|
import im.vector.app.features.analytics.extensions.toTrackingValue
|
||||||
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LateInitUserPropertiesFactory @Inject constructor(
|
||||||
|
private val activeSessionDataSource: ActiveSessionDataSource,
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
suspend fun createUserProperties(): UserProperties? {
|
||||||
|
val useCase = activeSessionDataSource.currentValue?.orNull()?.vectorStore(context)?.readUseCase()
|
||||||
|
return useCase?.let {
|
||||||
|
UserProperties(ftueUseCaseSelection = it.toTrackingValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.analytics.impl
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.posthog.android.PostHog
|
||||||
|
import im.vector.app.BuildConfig
|
||||||
|
import im.vector.app.config.analyticsConfig
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PostHogFactory @Inject constructor(private val context: Context) {
|
||||||
|
|
||||||
|
fun createPosthog(): PostHog {
|
||||||
|
return PostHog.Builder(context, analyticsConfig.postHogApiKey, analyticsConfig.postHogHost)
|
||||||
|
// Record certain application events automatically! (off/false by default)
|
||||||
|
// .captureApplicationLifecycleEvents()
|
||||||
|
// Record screen views automatically! (off/false by default)
|
||||||
|
// .recordScreenViews()
|
||||||
|
// Capture deep links as part of the screen call. (off by default)
|
||||||
|
// .captureDeepLinks()
|
||||||
|
// Maximum number of events to keep in queue before flushing (default 20)
|
||||||
|
// .flushQueueSize(20)
|
||||||
|
// Max delay before flushing the queue (30 seconds)
|
||||||
|
// .flushInterval(30, TimeUnit.SECONDS)
|
||||||
|
// Enable or disable collection of ANDROID_ID (true)
|
||||||
|
.collectDeviceId(false)
|
||||||
|
.logLevel(getLogLevel())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLogLevel(): PostHog.LogLevel {
|
||||||
|
return if (BuildConfig.DEBUG) {
|
||||||
|
PostHog.LogLevel.DEBUG
|
||||||
|
} else {
|
||||||
|
PostHog.LogLevel.INFO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,9 +22,9 @@ import im.vector.app.features.analytics.itf.VectorAnalyticsScreen
|
|||||||
// https://github.com/matrix-org/matrix-analytics-events/
|
// https://github.com/matrix-org/matrix-analytics-events/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered when the user changed screen
|
* Triggered when the user changed screen on Element Android/iOS
|
||||||
*/
|
*/
|
||||||
data class Screen(
|
data class MobileScreen(
|
||||||
/**
|
/**
|
||||||
* How long the screen was displayed for in milliseconds.
|
* How long the screen was displayed for in milliseconds.
|
||||||
*/
|
*/
|
||||||
@ -33,6 +33,11 @@ data class Screen(
|
|||||||
) : VectorAnalyticsScreen {
|
) : VectorAnalyticsScreen {
|
||||||
|
|
||||||
enum class ScreenName {
|
enum class ScreenName {
|
||||||
|
/**
|
||||||
|
* The screen that displays the user's breadcrumbs.
|
||||||
|
*/
|
||||||
|
Breadcrumbs,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The screen shown to create a new (non-direct) room.
|
* The screen shown to create a new (non-direct) room.
|
||||||
*/
|
*/
|
||||||
@ -43,6 +48,16 @@ data class Screen(
|
|||||||
*/
|
*/
|
||||||
DeactivateAccount,
|
DeactivateAccount,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab on mobile that displays the dialpad.
|
||||||
|
*/
|
||||||
|
Dialpad,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Favourites tab on mobile that lists your favourite people/rooms.
|
||||||
|
*/
|
||||||
|
Favourites,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The form for the forgot password use case
|
* The form for the forgot password use case
|
||||||
*/
|
*/
|
||||||
@ -54,11 +69,15 @@ data class Screen(
|
|||||||
Group,
|
Group,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Home tab on iOS | possibly the same on Android? | Home page on
|
* The Home tab on iOS | possibly the same on Android?
|
||||||
* Web
|
|
||||||
*/
|
*/
|
||||||
Home,
|
Home,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The screen shown to share a link to download the app.
|
||||||
|
*/
|
||||||
|
InviteFriends,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The screen that displays the login flow (when the user already has an
|
* The screen that displays the login flow (when the user already has an
|
||||||
* account).
|
* account).
|
||||||
@ -66,100 +85,14 @@ data class Screen(
|
|||||||
Login,
|
Login,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The screen that displays the user's breadcrumbs.
|
* Legacy: The screen that shows all groups/communities you have joined.
|
||||||
*/
|
*/
|
||||||
MobileBreadcrumbs,
|
MyGroups,
|
||||||
|
|
||||||
/**
|
|
||||||
* The tab on mobile that displays the dialpad.
|
|
||||||
*/
|
|
||||||
MobileDialpad,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Favourites tab on mobile that lists your favourite people/rooms.
|
|
||||||
*/
|
|
||||||
MobileFavourites,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The screen shown to share a link to download the app.
|
|
||||||
*/
|
|
||||||
MobileInviteFriends,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The People tab on mobile that lists all the DM rooms you have joined.
|
* The People tab on mobile that lists all the DM rooms you have joined.
|
||||||
*/
|
*/
|
||||||
MobilePeople,
|
People,
|
||||||
|
|
||||||
/**
|
|
||||||
* The Rooms tab on mobile that lists all the (non-direct) rooms you've
|
|
||||||
* joined.
|
|
||||||
*/
|
|
||||||
MobileRooms,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Files tab shown in the global search screen on Mobile.
|
|
||||||
*/
|
|
||||||
MobileSearchFiles,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Messages tab shown in the global search screen on Mobile.
|
|
||||||
*/
|
|
||||||
MobileSearchMessages,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The People tab shown in the global search screen on Mobile.
|
|
||||||
*/
|
|
||||||
MobileSearchPeople,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Rooms tab shown in the global search screen on Mobile.
|
|
||||||
*/
|
|
||||||
MobileSearchRooms,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The global settings screen shown in the app.
|
|
||||||
*/
|
|
||||||
MobileSettings,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The settings screen to change the default notification options.
|
|
||||||
*/
|
|
||||||
MobileSettingsDefaultNotifications,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The settings screen to manage notification mentions and keywords.
|
|
||||||
*/
|
|
||||||
MobileSettingsMentionsAndKeywords,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The global security settings screen.
|
|
||||||
*/
|
|
||||||
MobileSettingsSecurity,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The sidebar shown on mobile with spaces, settings etc.
|
|
||||||
*/
|
|
||||||
MobileSidebar,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Screen that displays the list of members of a space
|
|
||||||
*/
|
|
||||||
MobileSpaceMembers,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The bottom sheet that list all space options
|
|
||||||
*/
|
|
||||||
MobileSpaceMenu,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The screen shown to select which room directory you'd like to use.
|
|
||||||
*/
|
|
||||||
MobileSwitchDirectory,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy: The screen that shows all groups/communities you have joined.
|
|
||||||
*/
|
|
||||||
MyGroups,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The screen that displays the registration flow (when the user wants
|
* The screen that displays the registration flow (when the user wants
|
||||||
@ -216,107 +149,87 @@ data class Screen(
|
|||||||
*/
|
*/
|
||||||
RoomUploads,
|
RoomUploads,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Rooms tab on mobile that lists all the (non-direct) rooms you've
|
||||||
|
* joined.
|
||||||
|
*/
|
||||||
|
Rooms,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Files tab shown in the global search screen on Mobile.
|
||||||
|
*/
|
||||||
|
SearchFiles,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Messages tab shown in the global search screen on Mobile.
|
||||||
|
*/
|
||||||
|
SearchMessages,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The People tab shown in the global search screen on Mobile.
|
||||||
|
*/
|
||||||
|
SearchPeople,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Rooms tab shown in the global search screen on Mobile.
|
||||||
|
*/
|
||||||
|
SearchRooms,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The global settings screen shown in the app.
|
||||||
|
*/
|
||||||
|
Settings,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The settings screen to change the default notification options.
|
||||||
|
*/
|
||||||
|
SettingsDefaultNotifications,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The settings screen to manage notification mentions and keywords.
|
||||||
|
*/
|
||||||
|
SettingsMentionsAndKeywords,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The global security settings screen.
|
||||||
|
*/
|
||||||
|
SettingsSecurity,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sidebar shown on mobile with spaces, settings etc.
|
||||||
|
*/
|
||||||
|
Sidebar,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen that displays the list of rooms and spaces of a space
|
* Screen that displays the list of rooms and spaces of a space
|
||||||
*/
|
*/
|
||||||
SpaceExploreRooms,
|
SpaceExploreRooms,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen that displays the list of members of a space
|
||||||
|
*/
|
||||||
|
SpaceMembers,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bottom sheet that list all space options
|
||||||
|
*/
|
||||||
|
SpaceMenu,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The screen shown to create a new direct room.
|
* The screen shown to create a new direct room.
|
||||||
*/
|
*/
|
||||||
StartChat,
|
StartChat,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The screen shown to select which room directory you'd like to use.
|
||||||
|
*/
|
||||||
|
SwitchDirectory,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A screen that shows information about a room member.
|
* A screen that shows information about a room member.
|
||||||
*/
|
*/
|
||||||
User,
|
User,
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web showing flow to trust this new device with cross-signing.
|
|
||||||
*/
|
|
||||||
WebCompleteSecurity,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web showing flow to setup SSSS / cross-signing on this
|
|
||||||
* account.
|
|
||||||
*/
|
|
||||||
WebE2ESetup,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web loading spinner.
|
|
||||||
*/
|
|
||||||
WebLoading,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web device has been soft logged out by the server.
|
|
||||||
*/
|
|
||||||
WebSoftLogout,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy: Element Web User Settings Flair Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingFlair,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Mjolnir (labs) Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingMjolnir,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Appearance Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsAppearance,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings General Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsGeneral,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Help & About Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsHelpAbout,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Ignored Users Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsIgnoredUsers,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Keyboard Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsKeyboard,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Labs Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsLabs,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Notifications Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsNotifications,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Preferences Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsPreferences,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Security & Privacy Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsSecurityPrivacy,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Sidebar Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsSidebar,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Element Web User Settings Voice & Video Tab.
|
|
||||||
*/
|
|
||||||
WebUserSettingsVoiceVideo,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The splash screen.
|
* The splash screen.
|
||||||
*/
|
*/
|
@ -18,13 +18,13 @@ package im.vector.app.features.analytics.screen
|
|||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track a screen display. Unique usage.
|
* Track a screen display. Unique usage.
|
||||||
*/
|
*/
|
||||||
class ScreenEvent(val screenName: Screen.ScreenName) {
|
class ScreenEvent(val screenName: MobileScreen.ScreenName) {
|
||||||
private val startTime = SystemClock.elapsedRealtime()
|
private val startTime = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
// Protection to avoid multiple sending
|
// Protection to avoid multiple sending
|
||||||
@ -34,14 +34,14 @@ class ScreenEvent(val screenName: Screen.ScreenName) {
|
|||||||
* @param screenNameOverride can be used to override the screen name passed in constructor parameter
|
* @param screenNameOverride can be used to override the screen name passed in constructor parameter
|
||||||
*/
|
*/
|
||||||
fun send(analyticsTracker: AnalyticsTracker,
|
fun send(analyticsTracker: AnalyticsTracker,
|
||||||
screenNameOverride: Screen.ScreenName? = null) {
|
screenNameOverride: MobileScreen.ScreenName? = null) {
|
||||||
if (isSent) {
|
if (isSent) {
|
||||||
Timber.w("Event $screenName Already sent!")
|
Timber.w("Event $screenName Already sent!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isSent = true
|
isSent = true
|
||||||
analyticsTracker.screen(
|
analyticsTracker.screen(
|
||||||
Screen(
|
MobileScreen(
|
||||||
screenName = screenNameOverride ?: screenName,
|
screenName = screenNameOverride ?: screenName,
|
||||||
durationMs = (SystemClock.elapsedRealtime() - startTime).toInt()
|
durationMs = (SystemClock.elapsedRealtime() - startTime).toInt()
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.autocomplete
|
||||||
|
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_autocomplete_header_item)
|
||||||
|
abstract class AutocompleteHeaderItem : VectorEpoxyModel<AutocompleteHeaderItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute var title: String? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.titleView.text = title
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val titleView by bind<TextView>(R.id.headerItemAutocompleteTitle)
|
||||||
|
}
|
||||||
|
}
|
@ -16,31 +16,81 @@
|
|||||||
|
|
||||||
package im.vector.app.features.autocomplete.member
|
package im.vector.app.features.autocomplete.member
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
||||||
|
import im.vector.app.features.autocomplete.autocompleteHeaderItem
|
||||||
import im.vector.app.features.autocomplete.autocompleteMatrixItem
|
import im.vector.app.features.autocomplete.autocompleteMatrixItem
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AutocompleteMemberController @Inject constructor() : TypedEpoxyController<List<RoomMemberSummary>>() {
|
class AutocompleteMemberController @Inject constructor(private val context: Context) :
|
||||||
|
TypedEpoxyController<List<AutocompleteMemberItem>>() {
|
||||||
|
|
||||||
var listener: AutocompleteClickListener<RoomMemberSummary>? = null
|
/* ==========================================================================================
|
||||||
|
* Fields
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
var listener: AutocompleteClickListener<AutocompleteMemberItem>? = null
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Dependencies
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
|
||||||
override fun buildModels(data: List<RoomMemberSummary>?) {
|
/* ==========================================================================================
|
||||||
|
* Specialization
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
override fun buildModels(data: List<AutocompleteMemberItem>?) {
|
||||||
if (data.isNullOrEmpty()) {
|
if (data.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
data.forEach { item ->
|
||||||
|
when (item) {
|
||||||
|
is AutocompleteMemberItem.Header -> buildHeaderItem(item)
|
||||||
|
is AutocompleteMemberItem.RoomMember -> buildRoomMemberItem(item)
|
||||||
|
is AutocompleteMemberItem.Everyone -> buildEveryoneItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Helper methods
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
private fun buildHeaderItem(header: AutocompleteMemberItem.Header) {
|
||||||
|
autocompleteHeaderItem {
|
||||||
|
id(header.id)
|
||||||
|
title(header.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRoomMemberItem(roomMember: AutocompleteMemberItem.RoomMember) {
|
||||||
val host = this
|
val host = this
|
||||||
data.forEach { user ->
|
autocompleteMatrixItem {
|
||||||
autocompleteMatrixItem {
|
roomMember.roomMemberSummary.let { user ->
|
||||||
id(user.userId)
|
id(user.userId)
|
||||||
matrixItem(user.toMatrixItem())
|
matrixItem(user.toMatrixItem())
|
||||||
avatarRenderer(host.avatarRenderer)
|
avatarRenderer(host.avatarRenderer)
|
||||||
clickListener { host.listener?.onItemClick(user) }
|
clickListener { host.listener?.onItemClick(roomMember) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEveryoneItem(everyone: AutocompleteMemberItem.Everyone) {
|
||||||
|
val host = this
|
||||||
|
autocompleteMatrixItem {
|
||||||
|
everyone.roomSummary.let { room ->
|
||||||
|
id(room.roomId)
|
||||||
|
matrixItem(room.toEveryoneInRoomMatrixItem())
|
||||||
|
subName(host.context.getString(R.string.room_message_notify_everyone))
|
||||||
|
avatarRenderer(host.avatarRenderer)
|
||||||
|
clickListener { host.listener?.onItemClick(everyone) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.autocomplete.member
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
|
sealed class AutocompleteMemberItem {
|
||||||
|
data class Header(val id: String, val title: String) : AutocompleteMemberItem()
|
||||||
|
data class RoomMember(val roomMemberSummary: RoomMemberSummary) : AutocompleteMemberItem()
|
||||||
|
data class Everyone(val roomSummary: RoomSummary) : AutocompleteMemberItem()
|
||||||
|
}
|
@ -21,26 +21,44 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
||||||
import im.vector.app.features.autocomplete.RecyclerViewPresenter
|
import im.vector.app.features.autocomplete.RecyclerViewPresenter
|
||||||
|
import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
||||||
@Assisted val roomId: String,
|
@Assisted val roomId: String,
|
||||||
session: Session,
|
private val session: Session,
|
||||||
private val controller: AutocompleteMemberController
|
private val controller: AutocompleteMemberController
|
||||||
) : RecyclerViewPresenter<RoomMemberSummary>(context), AutocompleteClickListener<RoomMemberSummary> {
|
) : RecyclerViewPresenter<AutocompleteMemberItem>(context), AutocompleteClickListener<AutocompleteMemberItem> {
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Fields
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
private val room by lazy { session.getRoom(roomId)!! }
|
private val room by lazy { session.getRoom(roomId)!! }
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Init
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
init {
|
init {
|
||||||
controller.listener = this
|
controller.listener = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Public api
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
controller.listener = null
|
controller.listener = null
|
||||||
}
|
}
|
||||||
@ -50,29 +68,100 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
|
|||||||
fun create(roomId: String): AutocompleteMemberPresenter
|
fun create(roomId: String): AutocompleteMemberPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Specialization
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
|
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
|
||||||
return controller.adapter
|
return controller.adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(t: RoomMemberSummary) {
|
override fun onItemClick(t: AutocompleteMemberItem) {
|
||||||
dispatchClick(t)
|
dispatchClick(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQuery(query: CharSequence?) {
|
override fun onQuery(query: CharSequence?) {
|
||||||
val queryParams = roomMemberQueryParams {
|
val queryParams = createQueryParams(query)
|
||||||
displayName = if (query.isNullOrBlank()) {
|
val membersHeader = createMembersHeader()
|
||||||
QueryStringValue.IsNotEmpty
|
val members = createMemberItems(queryParams)
|
||||||
} else {
|
val everyone = createEveryoneItem(query)
|
||||||
QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
|
// 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()
|
controller.setData(items)
|
||||||
.sortedBy { it.displayName }
|
}
|
||||||
.disambiguate()
|
|
||||||
controller.setData(members.toList())
|
/* ==========================================================================================
|
||||||
|
* Helper methods
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
private fun createQueryParams(query: CharSequence?) = roomMemberQueryParams {
|
||||||
|
displayName = if (query.isNullOrBlank()) {
|
||||||
|
QueryStringValue.IsNotEmpty
|
||||||
|
} else {
|
||||||
|
QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
|
||||||
|
}
|
||||||
|
memberships = listOf(Membership.JOIN)
|
||||||
|
excludeSelf = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMembersHeader() =
|
||||||
|
AutocompleteMemberItem.Header(
|
||||||
|
ID_HEADER_MEMBERS,
|
||||||
|
context.getString(R.string.room_message_autocomplete_users)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createMemberItems(queryParams: RoomMemberQueryParams) =
|
||||||
|
room.getRoomMembers(queryParams)
|
||||||
|
.asSequence()
|
||||||
|
.sortedBy { it.displayName }
|
||||||
|
.disambiguate()
|
||||||
|
.map { AutocompleteMemberItem.RoomMember(it) }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
private fun createEveryoneHeader() =
|
||||||
|
AutocompleteMemberItem.Header(
|
||||||
|
ID_HEADER_EVERYONE,
|
||||||
|
context.getString(R.string.room_message_autocomplete_notification)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createEveryoneItem(query: CharSequence?) =
|
||||||
|
room.roomSummary()
|
||||||
|
?.takeIf { canNotifyEveryone() }
|
||||||
|
?.takeIf { query.isNullOrBlank() || MatrixItem.NOTIFY_EVERYONE.startsWith("@$query") }
|
||||||
|
?.let {
|
||||||
|
AutocompleteMemberItem.Everyone(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canNotifyEveryone() = session.resolveSenderNotificationPermissionCondition(
|
||||||
|
Event(
|
||||||
|
senderId = session.myUserId,
|
||||||
|
roomId = roomId
|
||||||
|
),
|
||||||
|
SenderNotificationPermissionCondition(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Const
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ID_HEADER_MEMBERS = "ID_HEADER_MEMBERS"
|
||||||
|
private const val ID_HEADER_EVERYONE = "ID_HEADER_EVERYONE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ import im.vector.app.core.utils.registerForPermissionsResult
|
|||||||
import im.vector.app.databinding.ActivityCallBinding
|
import im.vector.app.databinding.ActivityCallBinding
|
||||||
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
|
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
|
||||||
import im.vector.app.features.call.dialpad.DialPadFragment
|
import im.vector.app.features.call.dialpad.DialPadFragment
|
||||||
|
import im.vector.app.features.call.transfer.CallTransferActivity
|
||||||
import im.vector.app.features.call.utils.EglUtils
|
import im.vector.app.features.call.utils.EglUtils
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
@ -165,6 +166,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||||||
?.let {
|
?.let {
|
||||||
callViewModel.handle(VectorCallViewActions.SwitchCall(it))
|
callViewModel.handle(VectorCallViewActions.SwitchCall(it))
|
||||||
}
|
}
|
||||||
|
this.intent = intent
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMenuRes() = R.menu.vector_call
|
override fun getMenuRes() = R.menu.vector_call
|
||||||
@ -522,14 +524,21 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||||||
val callId = withState(callViewModel) { it.callId }
|
val callId = withState(callViewModel) { it.callId }
|
||||||
navigator.openCallTransfer(this, callTransferActivityResultLauncher, callId)
|
navigator.openCallTransfer(this, callTransferActivityResultLauncher, callId)
|
||||||
}
|
}
|
||||||
|
is VectorCallViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
|
||||||
null -> {
|
null -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callTransferActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
private val callTransferActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||||
if (activityResult.resultCode == Activity.RESULT_CANCELED) {
|
when (activityResult.resultCode) {
|
||||||
callViewModel.handle(VectorCallViewActions.CallTransferSelectionCancelled)
|
Activity.RESULT_CANCELED -> {
|
||||||
|
callViewModel.handle(VectorCallViewActions.CallTransferSelectionCancelled)
|
||||||
|
}
|
||||||
|
Activity.RESULT_OK -> {
|
||||||
|
CallTransferActivity.getCallTransferResult(activityResult.data)
|
||||||
|
?.let { callViewModel.handle(VectorCallViewActions.CallTransferSelectionResult(it)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.call
|
|||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
import im.vector.app.features.call.audio.CallAudioManager
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
|
import im.vector.app.features.call.transfer.CallTransferResult
|
||||||
|
|
||||||
sealed class VectorCallViewActions : VectorViewModelAction {
|
sealed class VectorCallViewActions : VectorViewModelAction {
|
||||||
object EndCall : VectorCallViewActions()
|
object EndCall : VectorCallViewActions()
|
||||||
@ -37,5 +38,6 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
|||||||
object ToggleHDSD : VectorCallViewActions()
|
object ToggleHDSD : VectorCallViewActions()
|
||||||
object InitiateCallTransfer : VectorCallViewActions()
|
object InitiateCallTransfer : VectorCallViewActions()
|
||||||
object CallTransferSelectionCancelled : VectorCallViewActions()
|
object CallTransferSelectionCancelled : VectorCallViewActions()
|
||||||
|
data class CallTransferSelectionResult(val callTransferResult: CallTransferResult) : VectorCallViewActions()
|
||||||
object TransferCall : VectorCallViewActions()
|
object TransferCall : VectorCallViewActions()
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
|
|||||||
) : VectorCallViewEvents()
|
) : VectorCallViewEvents()
|
||||||
object ShowDialPad : VectorCallViewEvents()
|
object ShowDialPad : VectorCallViewEvents()
|
||||||
object ShowCallTransferScreen : VectorCallViewEvents()
|
object ShowCallTransferScreen : VectorCallViewEvents()
|
||||||
|
object FailToTransfer : VectorCallViewEvents()
|
||||||
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||||
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
||||||
// object CallAccepted : VectorCallViewEvents()
|
// object CallAccepted : VectorCallViewEvents()
|
||||||
|
@ -29,13 +29,17 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
|||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.call.audio.CallAudioManager
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
|
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||||
|
import im.vector.app.features.call.transfer.CallTransferResult
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem
|
import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem
|
||||||
|
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
@ -47,7 +51,9 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||||||
@Assisted initialState: VectorCallViewState,
|
@Assisted initialState: VectorCallViewState,
|
||||||
val session: Session,
|
val session: Session,
|
||||||
val callManager: WebRtcCallManager,
|
val callManager: WebRtcCallManager,
|
||||||
val proximityManager: CallProximityManager
|
val proximityManager: CallProximityManager,
|
||||||
|
private val dialPadLookup: DialPadLookup,
|
||||||
|
private val directRoomHelper: DirectRoomHelper,
|
||||||
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
|
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
|
||||||
|
|
||||||
private var call: WebRtcCall? = null
|
private var call: WebRtcCall? = null
|
||||||
@ -327,6 +333,9 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||||||
VectorCallViewActions.CallTransferSelectionCancelled -> {
|
VectorCallViewActions.CallTransferSelectionCancelled -> {
|
||||||
call?.updateRemoteOnHold(false)
|
call?.updateRemoteOnHold(false)
|
||||||
}
|
}
|
||||||
|
is VectorCallViewActions.CallTransferSelectionResult -> {
|
||||||
|
handleCallTransferSelectionResult(action.callTransferResult)
|
||||||
|
}
|
||||||
VectorCallViewActions.TransferCall -> {
|
VectorCallViewActions.TransferCall -> {
|
||||||
handleCallTransfer()
|
handleCallTransfer()
|
||||||
}
|
}
|
||||||
@ -345,6 +354,53 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCallTransferSelectionResult(result: CallTransferResult) {
|
||||||
|
when (result) {
|
||||||
|
is CallTransferResult.ConnectWithUserId -> connectWithUserId(result)
|
||||||
|
is CallTransferResult.ConnectWithPhoneNumber -> connectWithPhoneNumber(result)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectWithUserId(result: CallTransferResult.ConnectWithUserId) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
if (result.consultFirst) {
|
||||||
|
val dmRoomId = directRoomHelper.ensureDMExists(result.selectedUserId)
|
||||||
|
callManager.startOutgoingCall(
|
||||||
|
nativeRoomId = dmRoomId,
|
||||||
|
otherUserId = result.selectedUserId,
|
||||||
|
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
|
||||||
|
transferee = call
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
call?.transferToUser(result.selectedUserId, null)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_viewEvents.post(VectorCallViewEvents.FailToTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectWithPhoneNumber(action: CallTransferResult.ConnectWithPhoneNumber) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
|
||||||
|
if (action.consultFirst) {
|
||||||
|
callManager.startOutgoingCall(
|
||||||
|
nativeRoomId = result.roomId,
|
||||||
|
otherUserId = result.userId,
|
||||||
|
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
|
||||||
|
transferee = call
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
call?.transferToUser(result.userId, result.roomId)
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_viewEvents.post(VectorCallViewEvents.FailToTransfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<VectorCallViewModel, VectorCallViewState> {
|
interface Factory : MavericksAssistedViewModelFactory<VectorCallViewModel, VectorCallViewState> {
|
||||||
override fun create(initialState: VectorCallViewState): VectorCallViewModel
|
override fun create(initialState: VectorCallViewState): VectorCallViewModel
|
||||||
|
@ -40,7 +40,7 @@ import com.android.dialer.dialpadview.DigitsEditText
|
|||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.singletonEntryPoint
|
import im.vector.app.core.extensions.singletonEntryPoint
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.analytics.screen.ScreenEvent
|
import im.vector.app.features.analytics.screen.ScreenEvent
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ class DialPadFragment : Fragment(), TextWatcher {
|
|||||||
private var screenEvent: ScreenEvent? = null
|
private var screenEvent: ScreenEvent? = null
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
screenEvent = ScreenEvent(Screen.ScreenName.MobileDialpad)
|
screenEvent = ScreenEvent(MobileScreen.ScreenName.Dialpad)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.call.transfer
|
package im.vector.app.features.call.transfer
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -27,6 +26,7 @@ import com.google.android.material.tabs.TabLayoutMediator
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.error.ErrorFormatter
|
import im.vector.app.core.error.ErrorFormatter
|
||||||
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivityCallTransferBinding
|
import im.vector.app.databinding.ActivityCallTransferBinding
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@ -56,10 +56,8 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>() {
|
|||||||
|
|
||||||
callTransferViewModel.observeViewEvents {
|
callTransferViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is CallTransferViewEvents.Complete -> handleComplete()
|
is CallTransferViewEvents.Complete -> handleComplete()
|
||||||
CallTransferViewEvents.Loading -> showWaitingView()
|
}.exhaustive
|
||||||
is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sectionsPagerAdapter = CallTransferPagerAdapter(this)
|
sectionsPagerAdapter = CallTransferPagerAdapter(this)
|
||||||
@ -82,29 +80,41 @@ class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>() {
|
|||||||
when (views.callTransferTabLayout.selectedTabPosition) {
|
when (views.callTransferTabLayout.selectedTabPosition) {
|
||||||
CallTransferPagerAdapter.USER_LIST_INDEX -> {
|
CallTransferPagerAdapter.USER_LIST_INDEX -> {
|
||||||
val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull() ?: return@debouncedClicks
|
val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull() ?: return@debouncedClicks
|
||||||
val action = CallTransferAction.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
|
val result = CallTransferResult.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
|
||||||
callTransferViewModel.handle(action)
|
handleComplete(result)
|
||||||
}
|
}
|
||||||
CallTransferPagerAdapter.DIAL_PAD_INDEX -> {
|
CallTransferPagerAdapter.DIAL_PAD_INDEX -> {
|
||||||
val phoneNumber = sectionsPagerAdapter.dialPadFragment?.getRawInput() ?: return@debouncedClicks
|
val phoneNumber = sectionsPagerAdapter.dialPadFragment?.getRawInput() ?: return@debouncedClicks
|
||||||
val action = CallTransferAction.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
|
val result = CallTransferResult.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
|
||||||
callTransferViewModel.handle(action)
|
handleComplete(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleComplete() {
|
private fun handleComplete(callTransferResult: CallTransferResult? = null) {
|
||||||
setResult(Activity.RESULT_OK)
|
if (callTransferResult != null) {
|
||||||
|
val intent = Intent().apply {
|
||||||
|
putExtra(EXTRA_TRANSFER_RESULT, callTransferResult)
|
||||||
|
}
|
||||||
|
setResult(RESULT_OK, intent)
|
||||||
|
} else {
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val EXTRA_TRANSFER_RESULT = "EXTRA_TRANSFER_RESULT"
|
||||||
|
|
||||||
fun newIntent(context: Context, callId: String): Intent {
|
fun newIntent(context: Context, callId: String): Intent {
|
||||||
return Intent(context, CallTransferActivity::class.java).also {
|
return Intent(context, CallTransferActivity::class.java).also {
|
||||||
it.putExtra(Mavericks.KEY_ARG, CallTransferArgs(callId))
|
it.putExtra(Mavericks.KEY_ARG, CallTransferArgs(callId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCallTransferResult(intent: Intent?): CallTransferResult? {
|
||||||
|
return intent?.extras?.getParcelable(EXTRA_TRANSFER_RESULT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,10 @@
|
|||||||
|
|
||||||
package im.vector.app.features.call.transfer
|
package im.vector.app.features.call.transfer
|
||||||
|
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
sealed class CallTransferAction : VectorViewModelAction {
|
sealed class CallTransferResult : Parcelable {
|
||||||
data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
|
@Parcelize data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferResult()
|
||||||
data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction()
|
@Parcelize data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferResult()
|
||||||
}
|
}
|
@ -20,6 +20,4 @@ import im.vector.app.core.platform.VectorViewEvents
|
|||||||
|
|
||||||
sealed class CallTransferViewEvents : VectorViewEvents {
|
sealed class CallTransferViewEvents : VectorViewEvents {
|
||||||
object Complete : CallTransferViewEvents()
|
object Complete : CallTransferViewEvents()
|
||||||
object Loading : CallTransferViewEvents()
|
|
||||||
object FailToTransfer : CallTransferViewEvents()
|
|
||||||
}
|
}
|
||||||
|
@ -22,22 +22,16 @@ import dagger.assisted.AssistedFactory
|
|||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.platform.EmptyAction
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
|
||||||
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
|
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
|
||||||
private val dialPadLookup: DialPadLookup,
|
|
||||||
private val directRoomHelper: DirectRoomHelper,
|
|
||||||
private val callManager: WebRtcCallManager) :
|
private val callManager: WebRtcCallManager) :
|
||||||
VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
|
VectorViewModel<CallTransferViewState, EmptyAction, CallTransferViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<CallTransferViewModel, CallTransferViewState> {
|
interface Factory : MavericksAssistedViewModelFactory<CallTransferViewModel, CallTransferViewState> {
|
||||||
@ -68,53 +62,5 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
|||||||
call?.removeListener(callListener)
|
call?.removeListener(callListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: CallTransferAction) {
|
override fun handle(action: EmptyAction) { }
|
||||||
when (action) {
|
|
||||||
is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
|
|
||||||
is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
|
|
||||||
}.exhaustive
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
if (action.consultFirst) {
|
|
||||||
val dmRoomId = directRoomHelper.ensureDMExists(action.selectedUserId)
|
|
||||||
callManager.startOutgoingCall(
|
|
||||||
nativeRoomId = dmRoomId,
|
|
||||||
otherUserId = action.selectedUserId,
|
|
||||||
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
|
|
||||||
transferee = call
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
call?.transferToUser(action.selectedUserId, null)
|
|
||||||
}
|
|
||||||
_viewEvents.post(CallTransferViewEvents.Complete)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun connectWithPhoneNumber(action: CallTransferAction.ConnectWithPhoneNumber) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
_viewEvents.post(CallTransferViewEvents.Loading)
|
|
||||||
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
|
|
||||||
if (action.consultFirst) {
|
|
||||||
callManager.startOutgoingCall(
|
|
||||||
nativeRoomId = result.roomId,
|
|
||||||
otherUserId = result.userId,
|
|
||||||
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
|
|
||||||
transferee = call
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
call?.transferToUser(result.userId, result.roomId)
|
|
||||||
}
|
|
||||||
_viewEvents.post(CallTransferViewEvents.Complete)
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
|||||||
import im.vector.app.core.utils.checkPermissions
|
import im.vector.app.core.utils.checkPermissions
|
||||||
import im.vector.app.core.utils.onPermissionDeniedSnackbar
|
import im.vector.app.core.utils.onPermissionDeniedSnackbar
|
||||||
import im.vector.app.core.utils.registerForPermissionsResult
|
import im.vector.app.core.utils.registerForPermissionsResult
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.contactsbook.ContactsBookFragment
|
import im.vector.app.features.contactsbook.ContactsBookFragment
|
||||||
import im.vector.app.features.qrcode.QrCodeScannerEvents
|
import im.vector.app.features.qrcode.QrCodeScannerEvents
|
||||||
import im.vector.app.features.qrcode.QrCodeScannerFragment
|
import im.vector.app.features.qrcode.QrCodeScannerFragment
|
||||||
@ -71,7 +71,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
analyticsScreenName = Screen.ScreenName.StartChat
|
analyticsScreenName = MobileScreen.ScreenName.StartChat
|
||||||
views.toolbar.visibility = View.GONE
|
views.toolbar.visibility = View.GONE
|
||||||
|
|
||||||
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
|
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
|
||||||
|
@ -48,7 +48,7 @@ import im.vector.app.databinding.ActivityHomeBinding
|
|||||||
import im.vector.app.features.MainActivity
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.MainActivityArgs
|
import im.vector.app.features.MainActivityArgs
|
||||||
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
|
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.analytics.screen.ScreenEvent
|
import im.vector.app.features.analytics.screen.ScreenEvent
|
||||||
import im.vector.app.features.disclaimer.showDisclaimerDialog
|
import im.vector.app.features.disclaimer.showDisclaimerDialog
|
||||||
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
import im.vector.app.features.matrixto.MatrixToBottomSheet
|
||||||
@ -165,7 +165,7 @@ class HomeActivity :
|
|||||||
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
||||||
private var drawerScreenEvent: ScreenEvent? = null
|
private var drawerScreenEvent: ScreenEvent? = null
|
||||||
override fun onDrawerOpened(drawerView: View) {
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileSidebar)
|
drawerScreenEvent = ScreenEvent(MobileScreen.ScreenName.Sidebar)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDrawerClosed(drawerView: View) {
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
@ -184,7 +184,7 @@ class HomeActivity :
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
analyticsScreenName = Screen.ScreenName.Home
|
analyticsScreenName = MobileScreen.ScreenName.Home
|
||||||
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
|
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
|
||||||
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
|
FcmHelper.ensureFcmTokenIsRetrieved(this, pushManager, vectorPreferences.areNotificationEnabledForDevice())
|
||||||
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
|
sharedActionViewModel = viewModelProvider.get(HomeSharedActionViewModel::class.java)
|
||||||
|
@ -457,7 +457,7 @@ class HomeDetailFragment @Inject constructor(
|
|||||||
backgroundColor = if (highlight) {
|
backgroundColor = if (highlight) {
|
||||||
ThemeUtils.getColor(requireContext(), R.attr.colorError)
|
ThemeUtils.getColor(requireContext(), R.attr.colorError)
|
||||||
} else {
|
} else {
|
||||||
ThemeUtils.getColor(requireContext(), R.attr.vctr_unread_room_badge)
|
ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import im.vector.app.core.extensions.replaceChildFragment
|
|||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.utils.startSharePlainTextIntent
|
import im.vector.app.core.utils.startSharePlainTextIntent
|
||||||
import im.vector.app.databinding.FragmentHomeDrawerBinding
|
import im.vector.app.databinding.FragmentHomeDrawerBinding
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.settings.VectorSettingsActivity
|
import im.vector.app.features.settings.VectorSettingsActivity
|
||||||
import im.vector.app.features.spaces.SpaceListFragment
|
import im.vector.app.features.spaces.SpaceListFragment
|
||||||
@ -98,7 +98,7 @@ class HomeDrawerFragment @Inject constructor(
|
|||||||
|
|
||||||
views.homeDrawerInviteFriendButton.debouncedClicks {
|
views.homeDrawerInviteFriendButton.debouncedClicks {
|
||||||
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
|
session.permalinkService().createPermalink(sharedActionViewModel.session.myUserId)?.let { permalink ->
|
||||||
analyticsTracker.screen(Screen(screenName = Screen.ScreenName.MobileInviteFriends))
|
analyticsTracker.screen(MobileScreen(screenName = MobileScreen.ScreenName.InviteFriends))
|
||||||
val text = getString(R.string.invite_friends_text, permalink)
|
val text = getString(R.string.invite_friends_text, permalink)
|
||||||
|
|
||||||
startSharePlainTextIntent(
|
startSharePlainTextIntent(
|
||||||
|
@ -33,6 +33,7 @@ import im.vector.app.features.autocomplete.command.AutocompleteCommandPresenter
|
|||||||
import im.vector.app.features.autocomplete.command.CommandAutocompletePolicy
|
import im.vector.app.features.autocomplete.command.CommandAutocompletePolicy
|
||||||
import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter
|
import im.vector.app.features.autocomplete.emoji.AutocompleteEmojiPresenter
|
||||||
import im.vector.app.features.autocomplete.group.AutocompleteGroupPresenter
|
import im.vector.app.features.autocomplete.group.AutocompleteGroupPresenter
|
||||||
|
import im.vector.app.features.autocomplete.member.AutocompleteMemberItem
|
||||||
import im.vector.app.features.autocomplete.member.AutocompleteMemberPresenter
|
import im.vector.app.features.autocomplete.member.AutocompleteMemberPresenter
|
||||||
import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter
|
import im.vector.app.features.autocomplete.room.AutocompleteRoomPresenter
|
||||||
import im.vector.app.features.command.Command
|
import im.vector.app.features.command.Command
|
||||||
@ -41,9 +42,9 @@ import im.vector.app.features.home.AvatarRenderer
|
|||||||
import im.vector.app.features.html.PillImageSpan
|
import im.vector.app.features.html.PillImageSpan
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
|
import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
Autocomplete.on<Command>(editText)
|
Autocomplete.on<Command>(editText)
|
||||||
.with(commandAutocompletePolicy)
|
.with(commandAutocompletePolicy)
|
||||||
.with(autocompleteCommandPresenter)
|
.with(autocompleteCommandPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<Command> {
|
.with(object : AutocompleteCallback<Command> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
|
||||||
@ -125,15 +126,24 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
|
private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
|
||||||
autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
|
autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
|
||||||
Autocomplete.on<RoomMemberSummary>(editText)
|
Autocomplete.on<AutocompleteMemberItem>(editText)
|
||||||
.with(CharPolicy('@', true))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_MEMBERS, true))
|
||||||
.with(autocompleteMemberPresenter)
|
.with(autocompleteMemberPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<RoomMemberSummary> {
|
.with(object : AutocompleteCallback<AutocompleteMemberItem> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: RoomMemberSummary): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: AutocompleteMemberItem): Boolean {
|
||||||
insertMatrixItem(editText, editable, "@", item.toMatrixItem())
|
return when (item) {
|
||||||
return true
|
is AutocompleteMemberItem.Header -> false // do nothing header is not clickable
|
||||||
|
is AutocompleteMemberItem.RoomMember -> {
|
||||||
|
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomMemberSummary.toMatrixItem())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
is AutocompleteMemberItem.Everyone -> {
|
||||||
|
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_MEMBERS, item.roomSummary.toEveryoneInRoomMatrixItem())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPopupVisibilityChanged(shown: Boolean) {
|
override fun onPopupVisibilityChanged(shown: Boolean) {
|
||||||
@ -144,13 +154,13 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
|
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
|
||||||
Autocomplete.on<RoomSummary>(editText)
|
Autocomplete.on<RoomSummary>(editText)
|
||||||
.with(CharPolicy('#', true))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_ROOMS, true))
|
||||||
.with(autocompleteRoomPresenter)
|
.with(autocompleteRoomPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<RoomSummary> {
|
.with(object : AutocompleteCallback<RoomSummary> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
|
||||||
insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
|
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_ROOMS, item.toRoomAliasMatrixItem())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,13 +172,13 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
|
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
|
||||||
Autocomplete.on<GroupSummary>(editText)
|
Autocomplete.on<GroupSummary>(editText)
|
||||||
.with(CharPolicy('+', true))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_GROUPS, true))
|
||||||
.with(autocompleteGroupPresenter)
|
.with(autocompleteGroupPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<GroupSummary> {
|
.with(object : AutocompleteCallback<GroupSummary> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
|
||||||
insertMatrixItem(editText, editable, "+", item.toMatrixItem())
|
insertMatrixItem(editText, editable, TRIGGER_AUTO_COMPLETE_GROUPS, item.toMatrixItem())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,9 +190,9 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
|
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
|
||||||
Autocomplete.on<String>(editText)
|
Autocomplete.on<String>(editText)
|
||||||
.with(CharPolicy(':', false))
|
.with(CharPolicy(TRIGGER_AUTO_COMPLETE_EMOJIS, false))
|
||||||
.with(autocompleteEmojiPresenter)
|
.with(autocompleteEmojiPresenter)
|
||||||
.with(ELEVATION)
|
.with(ELEVATION_DP)
|
||||||
.with(backgroundDrawable)
|
.with(backgroundDrawable)
|
||||||
.with(object : AutocompleteCallback<String> {
|
.with(object : AutocompleteCallback<String> {
|
||||||
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
|
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
|
||||||
@ -210,7 +220,7 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
|
private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: Char, matrixItem: MatrixItem) {
|
||||||
// Detect last firstChar and remove it
|
// Detect last firstChar and remove it
|
||||||
var startIndex = editable.lastIndexOf(firstChar)
|
var startIndex = editable.lastIndexOf(firstChar)
|
||||||
if (startIndex == -1) {
|
if (startIndex == -1) {
|
||||||
@ -228,7 +238,7 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
|
|
||||||
// Adding trailing space " " or ": " if the user started mention someone
|
// Adding trailing space " " or ": " if the user started mention someone
|
||||||
val displayNameSuffix =
|
val displayNameSuffix =
|
||||||
if (firstChar == "@" && startIndex == 0) {
|
if (matrixItem is MatrixItem.UserItem) {
|
||||||
": "
|
": "
|
||||||
} else {
|
} else {
|
||||||
" "
|
" "
|
||||||
@ -249,6 +259,10 @@ class AutoCompleter @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ELEVATION = 6f
|
private const val ELEVATION_DP = 6f
|
||||||
|
private const val TRIGGER_AUTO_COMPLETE_MEMBERS = '@'
|
||||||
|
private const val TRIGGER_AUTO_COMPLETE_ROOMS = '#'
|
||||||
|
private const val TRIGGER_AUTO_COMPLETE_GROUPS = '+'
|
||||||
|
private const val TRIGGER_AUTO_COMPLETE_EMOJIS = ':'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ import im.vector.app.core.extensions.keepScreenOn
|
|||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivityRoomDetailBinding
|
import im.vector.app.databinding.ActivityRoomDetailBinding
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.analytics.screen.ScreenEvent
|
import im.vector.app.features.analytics.screen.ScreenEvent
|
||||||
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
|
import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsFragment
|
||||||
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
||||||
@ -160,7 +160,7 @@ class RoomDetailActivity :
|
|||||||
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
|
||||||
private var drawerScreenEvent: ScreenEvent? = null
|
private var drawerScreenEvent: ScreenEvent? = null
|
||||||
override fun onDrawerOpened(drawerView: View) {
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
drawerScreenEvent = ScreenEvent(Screen.ScreenName.MobileBreadcrumbs)
|
drawerScreenEvent = ScreenEvent(MobileScreen.ScreenName.Breadcrumbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDrawerClosed(drawerView: View) {
|
override fun onDrawerClosed(drawerView: View) {
|
||||||
|
@ -29,7 +29,7 @@ import java.io.File
|
|||||||
* Transient events for RoomDetail
|
* Transient events for RoomDetail
|
||||||
*/
|
*/
|
||||||
sealed class RoomDetailViewEvents : VectorViewEvents {
|
sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||||
data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
|
data class Failure(val throwable: Throwable, val showInDialog: Boolean = false) : RoomDetailViewEvents()
|
||||||
data class OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
|
data class OnNewTimelineEvents(val eventIds: List<String>) : RoomDetailViewEvents()
|
||||||
|
|
||||||
data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents()
|
data class ActionSuccess(val action: RoomDetailAction) : RoomDetailViewEvents()
|
||||||
|
@ -49,6 +49,7 @@ data class JitsiState(
|
|||||||
data class RoomDetailViewState(
|
data class RoomDetailViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
|
val isInviteAlreadyAccepted: Boolean,
|
||||||
val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
|
val myRoomMember: Async<RoomMemberSummary> = Uninitialized,
|
||||||
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
val asyncInviter: Async<RoomMemberSummary> = Uninitialized,
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
@ -77,6 +78,7 @@ data class RoomDetailViewState(
|
|||||||
constructor(args: TimelineArgs) : this(
|
constructor(args: TimelineArgs) : this(
|
||||||
roomId = args.roomId,
|
roomId = args.roomId,
|
||||||
eventId = args.eventId,
|
eventId = args.eventId,
|
||||||
|
isInviteAlreadyAccepted = args.isInviteAlreadyAccepted,
|
||||||
// Also highlight the target event, if any
|
// Also highlight the target event, if any
|
||||||
highlightedEventId = args.eventId,
|
highlightedEventId = args.eventId,
|
||||||
switchToParentSpace = args.switchToParentSpace,
|
switchToParentSpace = args.switchToParentSpace,
|
||||||
|
@ -120,7 +120,7 @@ import im.vector.app.core.utils.toast
|
|||||||
import im.vector.app.databinding.DialogReportContentBinding
|
import im.vector.app.databinding.DialogReportContentBinding
|
||||||
import im.vector.app.databinding.FragmentTimelineBinding
|
import im.vector.app.databinding.FragmentTimelineBinding
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.attachments.AttachmentTypeSelectorView
|
import im.vector.app.features.attachments.AttachmentTypeSelectorView
|
||||||
import im.vector.app.features.attachments.AttachmentsHelper
|
import im.vector.app.features.attachments.AttachmentsHelper
|
||||||
import im.vector.app.features.attachments.ContactAttachment
|
import im.vector.app.features.attachments.ContactAttachment
|
||||||
@ -342,7 +342,7 @@ class TimelineFragment @Inject constructor(
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
analyticsScreenName = Screen.ScreenName.Room
|
analyticsScreenName = MobileScreen.ScreenName.Room
|
||||||
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
|
setFragmentResultListener(MigrateRoomBottomSheet.REQUEST_KEY) { _, bundle ->
|
||||||
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
|
bundle.getString(MigrateRoomBottomSheet.BUNDLE_KEY_REPLACEMENT_ROOM)?.let { replacementRoomId ->
|
||||||
timelineViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
|
timelineViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId))
|
||||||
@ -448,7 +448,7 @@ class TimelineFragment @Inject constructor(
|
|||||||
|
|
||||||
timelineViewModel.observeViewEvents {
|
timelineViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
is RoomDetailViewEvents.Failure -> displayErrorMessage(it)
|
||||||
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
|
||||||
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
|
||||||
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
|
||||||
@ -623,6 +623,10 @@ class TimelineFragment @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) {
|
||||||
|
if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable)
|
||||||
|
}
|
||||||
|
|
||||||
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
|
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
|
||||||
val tag = RoomWidgetPermissionBottomSheet::class.java.name
|
val tag = RoomWidgetPermissionBottomSheet::class.java.name
|
||||||
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
|
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
|
||||||
@ -2374,12 +2378,10 @@ class TimelineFragment @Inject constructor(
|
|||||||
|
|
||||||
// VectorInviteView.Callback
|
// VectorInviteView.Callback
|
||||||
override fun onAcceptInvite() {
|
override fun onAcceptInvite() {
|
||||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
|
|
||||||
timelineViewModel.handle(RoomDetailAction.AcceptInvite)
|
timelineViewModel.handle(RoomDetailAction.AcceptInvite)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRejectInvite() {
|
override fun onRejectInvite() {
|
||||||
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(timelineArgs.roomId) }
|
|
||||||
timelineViewModel.handle(RoomDetailAction.RejectInvite)
|
timelineViewModel.handle(RoomDetailAction.RejectInvite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandle
|
|||||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
import im.vector.app.features.home.room.typing.TypingHelper
|
import im.vector.app.features.home.room.typing.TypingHelper
|
||||||
|
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.settings.VectorDataStore
|
import im.vector.app.features.settings.VectorDataStore
|
||||||
@ -123,6 +124,7 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
private val analyticsTracker: AnalyticsTracker,
|
private val analyticsTracker: AnalyticsTracker,
|
||||||
private val activeConferenceHolder: JitsiActiveConferenceHolder,
|
private val activeConferenceHolder: JitsiActiveConferenceHolder,
|
||||||
private val decryptionFailureTracker: DecryptionFailureTracker,
|
private val decryptionFailureTracker: DecryptionFailureTracker,
|
||||||
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
timelineFactory: TimelineFactory,
|
timelineFactory: TimelineFactory,
|
||||||
appStateHandler: AppStateHandler
|
appStateHandler: AppStateHandler
|
||||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||||
@ -193,6 +195,11 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
prepareForEncryption()
|
prepareForEncryption()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user had already accepted the invitation in the room list
|
||||||
|
if (initialState.isInviteAlreadyAccepted) {
|
||||||
|
handleAcceptInvite()
|
||||||
|
}
|
||||||
|
|
||||||
if (initialState.switchToParentSpace) {
|
if (initialState.switchToParentSpace) {
|
||||||
// We are coming from a notification, try to switch to the most relevant space
|
// We are coming from a notification, try to switch to the most relevant space
|
||||||
// so that when hitting back the room will appear in the list
|
// so that when hitting back the room will appear in the list
|
||||||
@ -803,16 +810,24 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRejectInvite() {
|
private fun handleRejectInvite() {
|
||||||
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
tryOrNull { session.leaveRoom(room.roomId) }
|
try {
|
||||||
|
session.leaveRoom(room.roomId)
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
_viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAcceptInvite() {
|
private fun handleAcceptInvite() {
|
||||||
|
notificationDrawerManager.updateEvents { it.clearMemberShipNotificationForRoom(initialState.roomId) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
tryOrNull {
|
try {
|
||||||
session.joinRoom(room.roomId)
|
session.joinRoom(room.roomId)
|
||||||
analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom())
|
analyticsTracker.capture(room.roomSummary().toAnalyticsJoinedRoom())
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
_viewEvents.post(RoomDetailViewEvents.Failure(throwable, showInDialog = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,5 +28,6 @@ data class TimelineArgs(
|
|||||||
val sharedData: SharedData? = null,
|
val sharedData: SharedData? = null,
|
||||||
val openShareSpaceForId: String? = null,
|
val openShareSpaceForId: String? = null,
|
||||||
val threadTimelineArgs: ThreadTimelineArgs? = null,
|
val threadTimelineArgs: ThreadTimelineArgs? = null,
|
||||||
val switchToParentSpace: Boolean = false
|
val switchToParentSpace: Boolean = false,
|
||||||
|
val isInviteAlreadyAccepted: Boolean = false
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.factory
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.extensions.prevOrNull
|
import im.vector.app.core.extensions.prevOrNull
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
@ -26,10 +27,10 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisi
|
|||||||
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
|
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.MergedSimilarEventsItem
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.MergedSimilarEventsItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
@ -82,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||||||
event: TimelineEvent,
|
event: TimelineEvent,
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
requestModelBuild: () -> Unit,
|
requestModelBuild: () -> Unit,
|
||||||
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
|
callback: TimelineEventController.Callback?): MergedSimilarEventsItem_? {
|
||||||
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
|
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
|
||||||
items,
|
items,
|
||||||
currentPosition,
|
currentPosition,
|
||||||
@ -122,23 +123,31 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||||||
collapsedEventIds.removeAll(mergedEventIds)
|
collapsedEventIds.removeAll(mergedEventIds)
|
||||||
}
|
}
|
||||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||||
val attributes = MergedMembershipEventsItem.Attributes(
|
val summaryTitleResId = when (event.root.getClearType()) {
|
||||||
isCollapsed = isCollapsed,
|
EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes
|
||||||
mergeData = mergedData,
|
EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes
|
||||||
avatarRenderer = avatarRenderer,
|
else -> null
|
||||||
onCollapsedStateChanged = {
|
}
|
||||||
mergeItemCollapseStates[event.localId] = it
|
summaryTitleResId?.let { summaryTitle ->
|
||||||
requestModelBuild()
|
val attributes = MergedSimilarEventsItem.Attributes(
|
||||||
}
|
summaryTitleResId = summaryTitle,
|
||||||
)
|
isCollapsed = isCollapsed,
|
||||||
MergedMembershipEventsItem_()
|
mergeData = mergedData,
|
||||||
.id(mergeId)
|
avatarRenderer = avatarRenderer,
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
onCollapsedStateChanged = {
|
||||||
.highlighted(isCollapsed && highlighted)
|
mergeItemCollapseStates[event.localId] = it
|
||||||
.attributes(attributes)
|
requestModelBuild()
|
||||||
.also {
|
}
|
||||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
)
|
||||||
}
|
MergedSimilarEventsItem_()
|
||||||
|
.id(mergeId)
|
||||||
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
.highlighted(isCollapsed && highlighted)
|
||||||
|
.attributes(attributes)
|
||||||
|
.also {
|
||||||
|
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
|
|||||||
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
|
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
|
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_
|
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.render.EventTextRenderer
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||||
import im.vector.app.features.html.EventHtmlRenderer
|
import im.vector.app.features.html.EventHtmlRenderer
|
||||||
@ -112,6 +113,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||||
private val htmlRenderer: Lazy<EventHtmlRenderer>,
|
private val htmlRenderer: Lazy<EventHtmlRenderer>,
|
||||||
private val htmlCompressor: VectorHtmlCompressor,
|
private val htmlCompressor: VectorHtmlCompressor,
|
||||||
|
private val textRendererFactory: EventTextRenderer.Factory,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val imageContentRenderer: ImageContentRenderer,
|
private val imageContentRenderer: ImageContentRenderer,
|
||||||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||||
@ -138,6 +140,10 @@ class MessageItemFactory @Inject constructor(
|
|||||||
pillsPostProcessorFactory.create(roomId)
|
pillsPostProcessorFactory.create(roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val textRenderer by lazy {
|
||||||
|
textRendererFactory.create(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
|
||||||
val event = params.event
|
val event = params.event
|
||||||
val highlight = params.isHighlighted
|
val highlight = params.isHighlighted
|
||||||
@ -549,8 +555,9 @@ class MessageItemFactory @Inject constructor(
|
|||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||||
val bindingOptions = spanUtils.getBindingOptions(body)
|
val renderedBody = textRenderer.render(body)
|
||||||
val linkifiedBody = body.linkify(callback)
|
val bindingOptions = spanUtils.getBindingOptions(renderedBody)
|
||||||
|
val linkifiedBody = renderedBody.linkify(callback)
|
||||||
|
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(
|
.message(
|
||||||
|
@ -56,7 +56,8 @@ object TimelineDisplayableEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineEvent.canBeMerged(): Boolean {
|
fun TimelineEvent.canBeMerged(): Boolean {
|
||||||
return root.getClearType() == EventType.STATE_ROOM_MEMBER
|
return root.getClearType() == EventType.STATE_ROOM_MEMBER ||
|
||||||
|
root.getClearType() == EventType.STATE_ROOM_SERVER_ACL
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
|
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
|
||||||
|
@ -20,6 +20,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.PluralsRes
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
@ -27,7 +28,7 @@ import im.vector.app.R
|
|||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||||
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
abstract class MergedSimilarEventsItem : BasedMergedItem<MergedSimilarEventsItem.Holder>() {
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
|||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
if (attributes.isCollapsed) {
|
if (attributes.isCollapsed) {
|
||||||
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size)
|
val summary = holder.expandView.resources.getQuantityString(attributes.summaryTitleResId, attributes.mergeData.size, attributes.mergeData.size)
|
||||||
holder.summaryView.text = summary
|
holder.summaryView.text = summary
|
||||||
holder.summaryView.visibility = View.VISIBLE
|
holder.summaryView.visibility = View.VISIBLE
|
||||||
holder.avatarListView.visibility = View.VISIBLE
|
holder.avatarListView.visibility = View.VISIBLE
|
||||||
@ -66,6 +67,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Attributes(
|
data class Attributes(
|
||||||
|
@PluralsRes val summaryTitleResId: Int,
|
||||||
override val isCollapsed: Boolean,
|
override val isCollapsed: Boolean,
|
||||||
override val mergeData: List<Data>,
|
override val mergeData: List<Data>,
|
||||||
override val avatarRenderer: AvatarRenderer,
|
override val avatarRenderer: AvatarRenderer,
|
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.home.room.detail.timeline.render
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.core.glide.GlideApp
|
||||||
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.html.PillImageSpan
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
|
class EventTextRenderer @AssistedInject constructor(@Assisted private val roomId: String?,
|
||||||
|
private val context: Context,
|
||||||
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val sessionHolder: ActiveSessionHolder) {
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Public api
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(roomId: String?): EventTextRenderer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param text the text you want to render
|
||||||
|
*/
|
||||||
|
fun render(text: CharSequence): CharSequence {
|
||||||
|
return if (roomId != null && text.contains(MatrixItem.NOTIFY_EVERYONE)) {
|
||||||
|
SpannableStringBuilder(text).apply {
|
||||||
|
addNotifyEveryoneSpans(this, roomId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Helper methods
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
private fun addNotifyEveryoneSpans(text: Spannable, roomId: String) {
|
||||||
|
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomId)
|
||||||
|
val matrixItem = MatrixItem.EveryoneInRoomItem(
|
||||||
|
id = roomId,
|
||||||
|
avatarUrl = room?.avatarUrl,
|
||||||
|
roomDisplayName = room?.displayName
|
||||||
|
)
|
||||||
|
|
||||||
|
// search for notify everyone text
|
||||||
|
var foundIndex = text.indexOf(MatrixItem.NOTIFY_EVERYONE, 0)
|
||||||
|
while (foundIndex >= 0) {
|
||||||
|
val endSpan = foundIndex + MatrixItem.NOTIFY_EVERYONE.length
|
||||||
|
addPillSpan(text, createPillImageSpan(matrixItem), foundIndex, endSpan)
|
||||||
|
foundIndex = text.indexOf(MatrixItem.NOTIFY_EVERYONE, endSpan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPillImageSpan(matrixItem: MatrixItem) =
|
||||||
|
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
||||||
|
|
||||||
|
private fun addPillSpan(
|
||||||
|
renderedText: Spannable,
|
||||||
|
pillSpan: PillImageSpan,
|
||||||
|
startSpan: Int,
|
||||||
|
endSpan: Int
|
||||||
|
) {
|
||||||
|
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivityFilteredRoomsBinding
|
import im.vector.app.databinding.ActivityFilteredRoomsBinding
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.home.RoomListDisplayMode
|
import im.vector.app.features.home.RoomListDisplayMode
|
||||||
import im.vector.app.features.home.room.list.RoomListFragment
|
import im.vector.app.features.home.room.list.RoomListFragment
|
||||||
import im.vector.app.features.home.room.list.RoomListParams
|
import im.vector.app.features.home.room.list.RoomListParams
|
||||||
@ -43,7 +43,7 @@ class FilteredRoomsActivity : VectorBaseActivity<ActivityFilteredRoomsBinding>()
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
analyticsScreenName = Screen.ScreenName.RoomFilter
|
analyticsScreenName = MobileScreen.ScreenName.RoomFilter
|
||||||
setupToolbar(views.filteredRoomsToolbar)
|
setupToolbar(views.filteredRoomsToolbar)
|
||||||
.allowBack()
|
.allowBack()
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
|
@ -42,7 +42,7 @@ import im.vector.app.core.platform.StateView
|
|||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.resources.UserPreferencesProvider
|
import im.vector.app.core.resources.UserPreferencesProvider
|
||||||
import im.vector.app.databinding.FragmentRoomListBinding
|
import im.vector.app.databinding.FragmentRoomListBinding
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.home.RoomListDisplayMode
|
import im.vector.app.features.home.RoomListDisplayMode
|
||||||
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
|
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
|
||||||
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
|
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
|
||||||
@ -104,8 +104,8 @@ class RoomListFragment @Inject constructor(
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
analyticsScreenName = when (roomListParams.displayMode) {
|
analyticsScreenName = when (roomListParams.displayMode) {
|
||||||
RoomListDisplayMode.PEOPLE -> Screen.ScreenName.MobilePeople
|
RoomListDisplayMode.PEOPLE -> MobileScreen.ScreenName.People
|
||||||
RoomListDisplayMode.ROOMS -> Screen.ScreenName.MobileRooms
|
RoomListDisplayMode.ROOMS -> MobileScreen.ScreenName.Rooms
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,7 +121,7 @@ class RoomListFragment @Inject constructor(
|
|||||||
when (it) {
|
when (it) {
|
||||||
is RoomListViewEvents.Loading -> showLoading(it.message)
|
is RoomListViewEvents.Loading -> showLoading(it.message)
|
||||||
is RoomListViewEvents.Failure -> showFailure(it.throwable)
|
is RoomListViewEvents.Failure -> showFailure(it.throwable)
|
||||||
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
|
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it, it.isInviteAlreadyAccepted)
|
||||||
is RoomListViewEvents.Done -> Unit
|
is RoomListViewEvents.Done -> Unit
|
||||||
is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
|
is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
@ -184,8 +184,8 @@ class RoomListFragment @Inject constructor(
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) {
|
private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom, isInviteAlreadyAccepted: Boolean) {
|
||||||
navigator.openRoom(requireActivity(), event.roomSummary.roomId)
|
navigator.openRoom(context = requireActivity(), roomId = event.roomSummary.roomId, isInviteAlreadyAccepted = isInviteAlreadyAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupCreateRoomButton() {
|
private fun setupCreateRoomButton() {
|
||||||
|
@ -27,7 +27,7 @@ sealed class RoomListViewEvents : VectorViewEvents {
|
|||||||
data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
|
data class Loading(val message: CharSequence? = null) : RoomListViewEvents()
|
||||||
data class Failure(val throwable: Throwable) : RoomListViewEvents()
|
data class Failure(val throwable: Throwable) : RoomListViewEvents()
|
||||||
|
|
||||||
data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents()
|
data class SelectRoom(val roomSummary: RoomSummary, val isInviteAlreadyAccepted: Boolean = false) : RoomListViewEvents()
|
||||||
object Done : RoomListViewEvents()
|
object Done : RoomListViewEvents()
|
||||||
data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents()
|
data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,6 @@ import im.vector.app.core.extensions.exhaustive
|
|||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.analytics.AnalyticsTracker
|
import im.vector.app.features.analytics.AnalyticsTracker
|
||||||
import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
|
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.invite.AutoAcceptInvites
|
import im.vector.app.features.invite.AutoAcceptInvites
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
@ -174,7 +173,7 @@ class RoomListViewModel @AssistedInject constructor(
|
|||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
|
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {
|
||||||
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
|
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleToggleSection(roomSection: RoomsSection) {
|
private fun handleToggleSection(roomSection: RoomsSection) {
|
||||||
@ -208,6 +207,7 @@ class RoomListViewModel @AssistedInject constructor(
|
|||||||
Timber.w("Try to join an already joining room. Should not happen")
|
Timber.w("Try to join an already joining room. Should not happen")
|
||||||
return@withState
|
return@withState
|
||||||
}
|
}
|
||||||
|
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary, true))
|
||||||
|
|
||||||
// quick echo
|
// quick echo
|
||||||
setState {
|
setState {
|
||||||
@ -221,18 +221,6 @@ class RoomListViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
session.joinRoom(roomId)
|
|
||||||
analyticsTracker.capture(action.roomSummary.toAnalyticsJoinedRoom())
|
|
||||||
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
|
|
||||||
// Instead, we wait for the room to be joined
|
|
||||||
} catch (failure: Throwable) {
|
|
||||||
// Notify the user
|
|
||||||
_viewEvents.post(RoomListViewEvents.Failure(failure))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->
|
private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
package im.vector.app.features.html
|
package im.vector.app.features.html
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
@ -32,6 +33,7 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.glide.GlideRequests
|
import im.vector.app.core.glide.GlideRequests
|
||||||
import im.vector.app.features.displayname.getBestName
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
@ -117,6 +119,11 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
|||||||
setChipMinHeightResource(R.dimen.pill_min_height)
|
setChipMinHeightResource(R.dimen.pill_min_height)
|
||||||
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
||||||
chipIcon = icon
|
chipIcon = icon
|
||||||
|
if (matrixItem is MatrixItem.EveryoneInRoomItem) {
|
||||||
|
chipBackgroundColor = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.colorError))
|
||||||
|
// setTextColor API does not exist right now for ChipDrawable, use textAppearance
|
||||||
|
setTextAppearanceResource(R.style.TextAppearance_Vector_Body_OnError)
|
||||||
|
}
|
||||||
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,57 +36,87 @@ class PillsPostProcessor @AssistedInject constructor(@Assisted private val roomI
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val sessionHolder: ActiveSessionHolder) :
|
private val sessionHolder: ActiveSessionHolder) :
|
||||||
EventHtmlRenderer.PostProcessor {
|
EventHtmlRenderer.PostProcessor {
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Public api
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(roomId: String?): PillsPostProcessor
|
fun create(roomId: String?): PillsPostProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Specialization
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
override fun afterRender(renderedText: Spannable) {
|
override fun afterRender(renderedText: Spannable) {
|
||||||
addPillSpans(renderedText, roomId)
|
addPillSpans(renderedText, roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Helper methods
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
|
private fun addPillSpans(renderedText: Spannable, roomId: String?) {
|
||||||
|
addLinkSpans(renderedText, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addPillSpan(
|
||||||
|
renderedText: Spannable,
|
||||||
|
pillSpan: PillImageSpan,
|
||||||
|
startSpan: Int,
|
||||||
|
endSpan: Int
|
||||||
|
) {
|
||||||
|
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addLinkSpans(renderedText: Spannable, roomId: String?) {
|
||||||
// We let markdown handle links and then we add PillImageSpan if needed.
|
// We let markdown handle links and then we add PillImageSpan if needed.
|
||||||
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
|
val linkSpans = renderedText.getSpans(0, renderedText.length, LinkSpan::class.java)
|
||||||
linkSpans.forEach { linkSpan ->
|
linkSpans.forEach { linkSpan ->
|
||||||
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
val pillSpan = linkSpan.createPillSpan(roomId) ?: return@forEach
|
||||||
val startSpan = renderedText.getSpanStart(linkSpan)
|
val startSpan = renderedText.getSpanStart(linkSpan)
|
||||||
val endSpan = renderedText.getSpanEnd(linkSpan)
|
val endSpan = renderedText.getSpanEnd(linkSpan)
|
||||||
renderedText.setSpan(pillSpan, startSpan, endSpan, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
addPillSpan(renderedText, pillSpan, startSpan, endSpan)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createPillImageSpan(matrixItem: MatrixItem) =
|
||||||
|
PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
||||||
|
|
||||||
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
|
private fun LinkSpan.createPillSpan(roomId: String?): PillImageSpan? {
|
||||||
val permalinkData = PermalinkParser.parse(url)
|
val matrixItem = when (val permalinkData = PermalinkParser.parse(url)) {
|
||||||
val matrixItem = when (permalinkData) {
|
is PermalinkData.UserLink -> permalinkData.toMatrixItem(roomId)
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.RoomLink -> permalinkData.toMatrixItem()
|
||||||
if (roomId == null) {
|
is PermalinkData.GroupLink -> permalinkData.toMatrixItem()
|
||||||
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)
|
|
||||||
}
|
|
||||||
else -> null
|
else -> null
|
||||||
} ?: return null
|
} ?: return null
|
||||||
return PillImageSpan(GlideApp.with(context), avatarRenderer, context, matrixItem)
|
return createPillImageSpan(matrixItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PermalinkData.UserLink.toMatrixItem(roomId: String?): MatrixItem? =
|
||||||
|
if (roomId == null) {
|
||||||
|
sessionHolder.getSafeActiveSession()?.getUser(userId)?.toMatrixItem()
|
||||||
|
} else {
|
||||||
|
sessionHolder.getSafeActiveSession()?.getRoomMember(userId, roomId)?.toMatrixItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PermalinkData.RoomLink.toMatrixItem(): MatrixItem? =
|
||||||
|
if (eventId == null) {
|
||||||
|
val room: RoomSummary? = sessionHolder.getSafeActiveSession()?.getRoomSummary(roomIdOrAlias)
|
||||||
|
when {
|
||||||
|
isRoomAlias -> MatrixItem.RoomAliasItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||||
|
else -> MatrixItem.RoomItem(roomIdOrAlias, room?.displayName, room?.avatarUrl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exclude event link (used in reply events, we do not want to pill the "in reply to")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PermalinkData.GroupLink.toMatrixItem(): MatrixItem? {
|
||||||
|
val group = sessionHolder.getSafeActiveSession()?.getGroupSummary(groupId)
|
||||||
|
return MatrixItem.GroupItem(groupId, group?.displayName, group?.avatarUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack
|
|||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivityLoginBinding
|
import im.vector.app.databinding.ActivityLoginBinding
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.home.HomeActivity
|
import im.vector.app.features.home.HomeActivity
|
||||||
import im.vector.app.features.login.terms.LoginTermsFragment
|
import im.vector.app.features.login.terms.LoginTermsFragment
|
||||||
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
|
import im.vector.app.features.login.terms.LoginTermsFragmentArgument
|
||||||
@ -81,7 +81,7 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
|
|||||||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||||
|
|
||||||
override fun initUiAndData() {
|
override fun initUiAndData() {
|
||||||
analyticsScreenName = Screen.ScreenName.Login
|
analyticsScreenName = MobileScreen.ScreenName.Login
|
||||||
|
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
addFirstFragment()
|
addFirstFragment()
|
||||||
@ -203,7 +203,7 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), UnlockedA
|
|||||||
if (loginViewState.isUserLogged()) {
|
if (loginViewState.isUserLogged()) {
|
||||||
if (loginViewState.signMode == SignMode.SignUp) {
|
if (loginViewState.signMode == SignMode.SignUp) {
|
||||||
// change the screen name
|
// change the screen name
|
||||||
analyticsScreenName = Screen.ScreenName.Register
|
analyticsScreenName = MobileScreen.ScreenName.Register
|
||||||
}
|
}
|
||||||
val intent = HomeActivity.newIntent(
|
val intent = HomeActivity.newIntent(
|
||||||
this,
|
this,
|
||||||
|
@ -31,7 +31,7 @@ import im.vector.app.core.extensions.hidePassword
|
|||||||
import im.vector.app.core.extensions.isEmail
|
import im.vector.app.core.extensions.isEmail
|
||||||
import im.vector.app.core.extensions.toReducedUrl
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentLoginResetPasswordBinding
|
import im.vector.app.databinding.FragmentLoginResetPasswordBinding
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@ -48,7 +48,7 @@ class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment<F
|
|||||||
private var showWarning = true
|
private var showWarning = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
analyticsScreenName = Screen.ScreenName.ForgotPassword
|
analyticsScreenName = MobileScreen.ScreenName.ForgotPassword
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.databinding.FragmentLoginSplashBinding
|
import im.vector.app.databinding.FragmentLoginSplashBinding
|
||||||
import im.vector.app.features.analytics.plan.Screen
|
import im.vector.app.features.analytics.plan.MobileScreen
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
@ -44,7 +44,7 @@ class LoginSplashFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
analyticsScreenName = Screen.ScreenName.Welcome
|
analyticsScreenName = MobileScreen.ScreenName.Welcome
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user