Merge branch 'develop' into feature/event_deletion_dialog

# Conflicts:
#	CHANGES.md
#	vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
This commit is contained in:
onurays 2020-02-11 12:11:36 +03:00
commit afbd9cff70
55 changed files with 894 additions and 677 deletions

View File

@ -1,13 +1,30 @@
Changes in RiotX 0.15.0 (2020-XX-XX) Changes in RiotX 0.16.0 (2020-XX-XX)
=================================================== ===================================================
Features ✨: Features ✨:
- -
Improvements 🙌:
- Show confirmation dialog before deleting a message (#967)
Other changes:
-
Bugfix 🐛:
-
Translations 🗣:
-
Build 🧱:
-
Changes in RiotX 0.15.0 (2020-02-10)
===================================================
Improvements 🙌: Improvements 🙌:
- Improve navigation to the timeline (#789, #862) - Improve navigation to the timeline (#789, #862)
- Improve network detection. It is now based on the sync request status (#873, #882) - Improve network detection. It is now based on the sync request status (#873, #882)
- Show confirmation dialog before deleting a message (#967)
Other changes: Other changes:
- Support SSO login with Firefox account (#606) - Support SSO login with Firefox account (#606)
@ -16,12 +33,6 @@ Bugfix 🐛:
- Ask for permission before opening the camera (#934) - Ask for permission before opening the camera (#934)
- Encrypt for invited users by default, if the room state allows it (#803) - Encrypt for invited users by default, if the room state allows it (#803)
Translations 🗣:
-
Build 🧱:
-
Changes in RiotX 0.14.3 (2020-02-03) Changes in RiotX 0.14.3 (2020-02-03)
=================================================== ===================================================

View File

@ -15,30 +15,32 @@
*/ */
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
abstract class Condition(val kind: Kind) { abstract class Condition(val kind: Kind) {
enum class Kind(val value: String) { enum class Kind(val value: String) {
event_match("event_match"), EventMatch("event_match"),
contains_display_name("contains_display_name"), ContainsDisplayName("contains_display_name"),
room_member_count("room_member_count"), RoomMemberCount("room_member_count"),
sender_notification_permission("sender_notification_permission"), SenderNotificationPermission("sender_notification_permission"),
UNRECOGNIZE(""); Unrecognised("");
companion object { companion object {
fun fromString(value: String): Kind { fun fromString(value: String): Kind {
return when (value) { return when (value) {
"event_match" -> event_match "event_match" -> EventMatch
"contains_display_name" -> contains_display_name "contains_display_name" -> ContainsDisplayName
"room_member_count" -> room_member_count "room_member_count" -> RoomMemberCount
"sender_notification_permission" -> sender_notification_permission "sender_notification_permission" -> SenderNotificationPermission
else -> UNRECOGNIZE else -> Unrecognised
} }
} }
} }
} }
abstract fun isSatisfied(conditionResolver: ConditionResolver): Boolean abstract fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean
open fun technicalDescription(): String { open fun technicalDescription(): String {
return "Kind: $kind" return "Kind: $kind"

View File

@ -15,14 +15,22 @@
*/ */
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
/** /**
* Acts like a visitor on Conditions. * Acts like a visitor on Conditions.
* This class as all required context needed to evaluate rules * This class as all required context needed to evaluate rules
*/ */
interface ConditionResolver { interface ConditionResolver {
fun resolveEventMatchCondition(event: Event,
condition: EventMatchCondition): Boolean
fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean fun resolveRoomMemberCountCondition(event: Event,
fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean condition: RoomMemberCountCondition): Boolean
fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition) : Boolean fun resolveSenderNotificationPermissionCondition(event: Event,
condition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(event: Event,
condition: ContainsDisplayNameCondition): Boolean
} }

View File

@ -21,10 +21,10 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import timber.log.Timber import timber.log.Timber
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) { class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(this) return conditionResolver.resolveContainsDisplayNameCondition(event, this)
} }
override fun technicalDescription(): String { override fun technicalDescription(): String {

View File

@ -19,10 +19,20 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import timber.log.Timber import timber.log.Timber
class EventMatchCondition(val key: String, val pattern: String) : Condition(Kind.event_match) { class EventMatchCondition(
/**
* The dot-separated field of the event to match, e.g. content.body
*/
val key: String,
/**
* The glob-style pattern to match against. Patterns with no special glob characters should
* be treated as having asterisks prepended and appended when testing the condition.
*/
val pattern: String
) : Condition(Kind.EventMatch) {
override fun isSatisfied(conditionResolver: ConditionResolver) : Boolean { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveEventMatchCondition(this) return conditionResolver.resolveEventMatchCondition(event, this)
} }
override fun technicalDescription(): String { override fun technicalDescription(): String {

View File

@ -16,25 +16,32 @@
package im.vector.matrix.android.api.pushrules package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.internal.session.room.RoomGetter
import timber.log.Timber import timber.log.Timber
private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$") private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$")
class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) { class RoomMemberCountCondition(
/**
* A decimal integer optionally prefixed by one of ==, <, >, >= or <=.
* A prefix of < matches rooms where the member count is strictly less than the given number and so forth.
* If no prefix is present, this parameter defaults to ==.
*/
val iz: String
) : Condition(Kind.RoomMemberCount) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this) return conditionResolver.resolveRoomMemberCountCondition(event, this)
} }
override fun technicalDescription(): String { override fun technicalDescription(): String {
return "Room member count is $iz" return "Room member count is $iz"
} }
fun isSatisfied(event: Event, session: RoomService?): Boolean { internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean {
// sanity check^ // sanity checks
val roomId = event.roomId ?: return false val roomId = event.roomId ?: return false
val room = session?.getRoom(roomId) ?: return false val room = roomGetter.getRoom(roomId) ?: return false
// Parse the is field into prefix and number the first time // Parse the is field into prefix and number the first time
val (prefix, count) = parseIsField() ?: return false val (prefix, count) = parseIsField() ?: return false

View File

@ -19,10 +19,18 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
class SenderNotificationPermissionCondition(val key: String) : Condition(Kind.sender_notification_permission) { class SenderNotificationPermissionCondition(
/**
* A string that determines the power level the sender must have to trigger notifications of a given type,
* such as room. Refer to the m.room.power_levels event schema for information about what the defaults are
* and how to interpret the event. The key is used to look up the power level required to send a notification
* type from the notifications object in the power level event content.
*/
val key: String
) : Condition(Kind.SenderNotificationPermission) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(this) return conditionResolver.resolveSenderNotificationPermissionCondition(event, this)
} }
override fun technicalDescription(): String { override fun technicalDescription(): String {

View File

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
* All push rulesets for a user. * All push rulesets for a user.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class GetPushRulesResponse( internal data class GetPushRulesResponse(
/** /**
* Global rules, account level applying to all devices * Global rules, account level applying to all devices
*/ */

View File

@ -17,7 +17,11 @@ package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.pushrules.* import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.ContainsDisplayNameCondition
import im.vector.matrix.android.api.pushrules.EventMatchCondition
import im.vector.matrix.android.api.pushrules.RoomMemberCountCondition
import im.vector.matrix.android.api.pushrules.SenderNotificationPermissionCondition
import timber.log.Timber import timber.log.Timber
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -30,13 +34,13 @@ data class PushCondition(
/** /**
* Required for event_match conditions. The dot- separated field of the event to match. * Required for event_match conditions. The dot- separated field of the event to match.
*/ */
val key: String? = null, val key: String? = null,
/**
*Required for event_match conditions.
*/
/**
* Required for event_match conditions.
*/
val pattern: String? = null, val pattern: String? = null,
/** /**
* Required for room_member_count conditions. * Required for room_member_count conditions.
* A decimal integer optionally prefixed by one of, ==, <, >, >= or <=. * A decimal integer optionally prefixed by one of, ==, <, >, >= or <=.
@ -47,30 +51,35 @@ data class PushCondition(
) { ) {
fun asExecutableCondition(): Condition? { fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(this.kind)) { return when (Condition.Kind.fromString(kind)) {
Condition.Kind.event_match -> { Condition.Kind.EventMatch -> {
if (this.key != null && this.pattern != null) { if (key != null && pattern != null) {
EventMatchCondition(key, pattern) EventMatchCondition(key, pattern)
} else { } else {
Timber.e("Malformed Event match condition") Timber.e("Malformed Event match condition")
null null
} }
} }
Condition.Kind.contains_display_name -> { Condition.Kind.ContainsDisplayName -> {
ContainsDisplayNameCondition() ContainsDisplayNameCondition()
} }
Condition.Kind.room_member_count -> { Condition.Kind.RoomMemberCount -> {
if (this.iz.isNullOrBlank()) { if (iz.isNullOrEmpty()) {
Timber.e("Malformed ROOM_MEMBER_COUNT condition") Timber.e("Malformed ROOM_MEMBER_COUNT condition")
null null
} else { } else {
RoomMemberCountCondition(this.iz) RoomMemberCountCondition(iz)
} }
} }
Condition.Kind.sender_notification_permission -> { Condition.Kind.SenderNotificationPermission -> {
this.key?.let { SenderNotificationPermissionCondition(it) } if (key == null) {
Timber.e("Malformed Sender Notification Permission condition")
null
} else {
SenderNotificationPermissionCondition(key)
}
} }
Condition.Kind.UNRECOGNIZE -> { Condition.Kind.Unrecognised -> {
Timber.e("Unknown kind $kind") Timber.e("Unknown kind $kind")
null null
} }

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Ruleset( internal data class Ruleset(
val content: List<PushRule>? = null, val content: List<PushRule>? = null,
val override: List<PushRule>? = null, val override: List<PushRule>? = null,
val room: List<PushRule>? = null, val room: List<PushRule>? = null,

View File

@ -110,9 +110,11 @@ internal class ShieldTrustUpdater @Inject constructor(
val map = HashMap<String, List<String>>() val map = HashMap<String, List<String>>()
impactedRoomsId.forEach { roomId -> impactedRoomsId.forEach { roomId ->
RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId).findAll()?.let { results -> RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId)
map[roomId] = results.map { it.userId } .findAll()
} .let { results ->
map[roomId] = results.map { it.userId }
}
} }
map.forEach { entry -> map.forEach { entry ->

View File

@ -38,7 +38,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled, enabled = pushrule.enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.ruleId,
conditions = listOf( conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "content.body", pushrule.pattern) PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
) )
) )
} }
@ -59,7 +59,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled, enabled = pushrule.enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.ruleId,
conditions = listOf( conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "room_id", pushrule.ruleId) PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId)
) )
) )
} }
@ -71,7 +71,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled, enabled = pushrule.enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.ruleId,
conditions = listOf( conditions = listOf(
PushCondition(Condition.Kind.event_match.name, "user_id", pushrule.ruleId) PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId)
) )
) )
} }

View File

@ -38,12 +38,13 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
internal class DefaultPushRuleService @Inject constructor(private val getPushRulesTask: GetPushRulesTask, internal class DefaultPushRuleService @Inject constructor(
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, private val getPushRulesTask: GetPushRulesTask,
private val addPushRuleTask: AddPushRuleTask, private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val removePushRuleTask: RemovePushRuleTask, private val addPushRuleTask: AddPushRuleTask,
private val taskExecutor: TaskExecutor, private val removePushRuleTask: RemovePushRuleTask,
private val monarchy: Monarchy private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
) : PushRuleService { ) : PushRuleService {
private var listeners = mutableSetOf<PushRuleService.PushRuleListener>() private var listeners = mutableSetOf<PushRuleService.PushRuleListener>()

View File

@ -16,12 +16,11 @@
package im.vector.matrix.android.internal.session.notification package im.vector.matrix.android.internal.session.notification
import im.vector.matrix.android.api.pushrules.ConditionResolver
import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.pushers.DefaultConditionResolver
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import timber.log.Timber import timber.log.Timber
@ -36,7 +35,7 @@ internal interface ProcessEventForPushTask : Task<ProcessEventForPushTask.Params
internal class DefaultProcessEventForPushTask @Inject constructor( internal class DefaultProcessEventForPushTask @Inject constructor(
private val defaultPushRuleService: DefaultPushRuleService, private val defaultPushRuleService: DefaultPushRuleService,
private val roomService: RoomService, private val conditionResolver: ConditionResolver,
@UserId private val userId: String @UserId private val userId: String
) : ProcessEventForPushTask { ) : ProcessEventForPushTask {
@ -97,12 +96,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
} }
private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? { private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? {
// TODO This should be injected
val conditionResolver = DefaultConditionResolver(event, roomService, userId)
return rules.firstOrNull { rule -> return rules.firstOrNull { rule ->
// All conditions must hold true for an event in order to apply the action for the event. // All conditions must hold true for an event in order to apply the action for the event.
rule.enabled && rule.conditions?.all { rule.enabled && rule.conditions?.all {
it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false
} ?: false } ?: false
} }
} }

View File

@ -15,37 +15,52 @@
*/ */
package im.vector.matrix.android.internal.session.pushers package im.vector.matrix.android.internal.session.pushers
import im.vector.matrix.android.api.pushrules.* import im.vector.matrix.android.api.pushrules.ConditionResolver
import im.vector.matrix.android.api.pushrules.ContainsDisplayNameCondition
import im.vector.matrix.android.api.pushrules.EventMatchCondition
import im.vector.matrix.android.api.pushrules.RoomMemberCountCondition
import im.vector.matrix.android.api.pushrules.SenderNotificationPermissionCondition
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import timber.log.Timber import im.vector.matrix.android.internal.session.room.RoomGetter
import javax.inject.Inject
// TODO Inject constructor internal class DefaultConditionResolver @Inject constructor(
internal class DefaultConditionResolver(private val event: Event, private val roomGetter: RoomGetter,
private val roomService: RoomService, @UserId private val userId: String
@UserId private val userId: String) : ConditionResolver { ) : ConditionResolver {
override fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean { override fun resolveEventMatchCondition(event: Event,
return eventMatchCondition.isSatisfied(event) condition: EventMatchCondition): Boolean {
return condition.isSatisfied(event)
} }
override fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean { override fun resolveRoomMemberCountCondition(event: Event,
return roomMemberCountCondition.isSatisfied(event, roomService) condition: RoomMemberCountCondition): Boolean {
return condition.isSatisfied(event, roomGetter)
} }
override fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean { override fun resolveSenderNotificationPermissionCondition(event: Event,
// val roomId = event.roomId ?: return false condition: SenderNotificationPermissionCondition): Boolean {
// val room = roomService.getRoom(roomId) ?: return false
// TODO RoomState not yet managed
Timber.e("POWER LEVELS STATE NOT YET MANAGED BY RIOTX")
return false // senderNotificationPermissionCondition.isSatisfied(event, )
}
override fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition): Boolean {
val roomId = event.roomId ?: return false val roomId = event.roomId ?: return false
val room = roomService.getRoom(roomId) ?: return false val room = roomGetter.getRoom(roomId) ?: return false
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
?.content
?.toModel<PowerLevelsContent>()
?: PowerLevelsContent()
return condition.isSatisfied(event, powerLevelsContent)
}
override fun resolveContainsDisplayNameCondition(event: Event,
condition: ContainsDisplayNameCondition): Boolean {
val roomId = event.roomId ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
val myDisplayName = room.getRoomMember(userId)?.displayName ?: return false val myDisplayName = room.getRoomMember(userId)?.displayName ?: return false
return containsDisplayNameCondition.isSatisfied(event, myDisplayName) return condition.isSatisfied(event, myDisplayName)
} }
} }

View File

@ -22,14 +22,12 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.findByAlias import im.vector.matrix.android.internal.database.query.findByAlias
@ -37,7 +35,6 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
@ -48,15 +45,17 @@ import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, internal class DefaultRoomService @Inject constructor(
private val roomSummaryMapper: RoomSummaryMapper, private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask, private val roomSummaryMapper: RoomSummaryMapper,
private val joinRoomTask: JoinRoomTask, private val createRoomTask: CreateRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val joinRoomTask: JoinRoomTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomFactory: RoomFactory, private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val taskExecutor: TaskExecutor) : RoomService { private val roomGetter: RoomGetter,
private val taskExecutor: TaskExecutor
) : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable { override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
return createRoomTask return createRoomTask
@ -67,33 +66,11 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
} }
override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use { return roomGetter.getRoom(roomId)
if (RoomEntity.where(it, roomId).findFirst() != null) {
roomFactory.create(roomId)
} else {
null
}
}
} }
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
Realm.getInstance(monarchy.realmConfiguration).use { realm -> return roomGetter.getDirectRoomWith(otherUserId)
val candidates = RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.findAll()?.filter { dm ->
dm.otherMemberIds.contains(otherUserId)
&& dm.membership == Membership.JOIN
}?.map {
it.roomId
}
?: return null
candidates.forEach { roomId ->
if (RoomMemberHelper(realm, roomId).getActiveRoomMemberIds().any { it == otherUserId }) {
return RoomEntity.where(realm, roomId).findFirst()?.let { roomFactory.create(roomId) }
}
}
return null
}
} }
override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { override fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import io.realm.Realm
import javax.inject.Inject
internal interface RoomGetter {
fun getRoom(roomId: String): Room?
fun getDirectRoomWith(otherUserId: String): Room?
}
@SessionScope
internal class DefaultRoomGetter @Inject constructor(
private val monarchy: Monarchy,
private val roomFactory: RoomFactory
) : RoomGetter {
override fun getRoom(roomId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
createRoom(realm, roomId)
}
}
override fun getDirectRoomWith(otherUserId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
.findAll()
.filter { dm -> dm.otherMemberIds.contains(otherUserId) }
.map { it.roomId }
.firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() }
?.let { roomId -> createRoom(realm, roomId) }
}
}
private fun createRoom(realm: Realm, roomId: String): Room? {
return RoomEntity.where(realm, roomId).findFirst()
?.let { roomFactory.create(roomId) }
}
}

View File

@ -46,12 +46,20 @@ import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsRe
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.relation.* import im.vector.matrix.android.internal.session.room.relation.DefaultFetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.timeline.* import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
import im.vector.matrix.android.internal.session.room.typing.SendTypingTask import im.vector.matrix.android.internal.session.room.typing.SendTypingTask
import retrofit2.Retrofit import retrofit2.Retrofit
@ -72,6 +80,9 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindRoomFactory(factory: DefaultRoomFactory): RoomFactory abstract fun bindRoomFactory(factory: DefaultRoomFactory): RoomFactory
@Binds
abstract fun bindRoomGetter(getter: DefaultRoomGetter): RoomGetter
@Binds @Binds
abstract fun bindRoomService(service: DefaultRoomService): RoomService abstract fun bindRoomService(service: DefaultRoomService): RoomService

View File

@ -54,7 +54,7 @@ internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule?
} }
else -> { else -> {
val condition = PushCondition( val condition = PushCondition(
kind = Condition.Kind.event_match.value, kind = Condition.Kind.EventMatch.value,
key = "room_id", key = "room_id",
pattern = roomId pattern = roomId
) )

View File

@ -19,18 +19,23 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.internal.session.room.RoomGetter
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.amshove.kluent.shouldBe
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
class PushrulesConditionTest { class PushrulesConditionTest {
/* ==========================================================================================
* Test EventMatchCondition
* ========================================================================================== */
@Test @Test
fun test_eventmatch_type_condition() { fun test_eventmatch_type_condition() {
val condition = EventMatchCondition("type", "m.room.message") val condition = EventMatchCondition("type", "m.room.message")
@ -120,6 +125,24 @@ class PushrulesConditionTest {
assert(condition.isSatisfied(simpleTextEvent)) assert(condition.isSatisfied(simpleTextEvent))
} }
@Test
fun test_notice_condition() {
val conditionEqual = EventMatchCondition("content.msgtype", "m.notice")
Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.notice", "A").toContent(),
originServerTs = 0,
roomId = "2joined").also {
assertTrue("Notice", conditionEqual.isSatisfied(it))
}
}
/* ==========================================================================================
* Test RoomMemberCountCondition
* ========================================================================================== */
@Test @Test
fun test_roommember_condition() { fun test_roommember_condition() {
val conditionEqual3 = RoomMemberCountCondition("3") val conditionEqual3 = RoomMemberCountCondition("3")
@ -137,7 +160,7 @@ class PushrulesConditionTest {
every { getNumberOfJoinedMembers() } returns 3 every { getNumberOfJoinedMembers() } returns 3
} }
val sessionStub = mockk<RoomService> { val roomGetterStub = mockk<RoomGetter> {
every { getRoom(room2JoinedId) } returns roomStub2Joined every { getRoom(room2JoinedId) } returns roomStub2Joined
every { getRoom(room3JoinedId) } returns roomStub3Joined every { getRoom(room3JoinedId) } returns roomStub3Joined
} }
@ -148,9 +171,9 @@ class PushrulesConditionTest {
content = MessageTextContent("m.text", "A").toContent(), content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0, originServerTs = 0,
roomId = room2JoinedId).also { roomId = room2JoinedId).also {
assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, sessionStub)) assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, roomGetterStub))
assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub)) assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub))
assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, sessionStub)) assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub))
} }
Event( Event(
@ -159,23 +182,36 @@ class PushrulesConditionTest {
content = MessageTextContent("m.text", "A").toContent(), content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0, originServerTs = 0,
roomId = room3JoinedId).also { roomId = room3JoinedId).also {
assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, sessionStub)) assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, roomGetterStub))
assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub)) assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub))
assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, sessionStub)) assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub))
} }
} }
/* ==========================================================================================
* Test ContainsDisplayNameCondition
* ========================================================================================== */
@Test @Test
fun test_notice_condition() { fun test_displayName_condition() {
val conditionEqual = EventMatchCondition("content.msgtype", "m.notice") val condition = ContainsDisplayNameCondition()
Event( val event = Event(
type = "m.room.message", type = "m.room.message",
eventId = "mx0", eventId = "mx0",
content = MessageTextContent("m.notice", "A").toContent(), content = MessageTextContent("m.text", "How was the cake benoit?").toContent(),
originServerTs = 0, originServerTs = 0,
roomId = "2joined").also { roomId = "2joined")
assertTrue("Notice", conditionEqual.isSatisfied(it))
} condition.isSatisfied(event, "how") shouldBe true
condition.isSatisfied(event, "How") shouldBe true
condition.isSatisfied(event, "benoit") shouldBe true
condition.isSatisfied(event, "Benoit") shouldBe true
condition.isSatisfied(event, "cake") shouldBe true
condition.isSatisfied(event, "ben") shouldBe false
condition.isSatisfied(event, "oit") shouldBe false
condition.isSatisfied(event, "enoi") shouldBe false
condition.isSatisfied(event, "H") shouldBe false
} }
} }

View File

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 15 ext.versionMinor = 16
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {

View File

@ -37,6 +37,9 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import timber.log.Timber import timber.log.Timber
/** /**
@ -87,10 +90,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
return view return view
} }
@CallSuper
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
unBinder?.unbind() unBinder?.unbind()
unBinder = null unBinder = null
uiDisposables.clear()
}
@CallSuper
override fun onDestroy() {
uiDisposables.dispose()
super.onDestroy()
} }
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
@ -146,4 +157,29 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
protected fun setArguments(args: Parcelable? = null) { protected fun setArguments(args: Parcelable? = null) {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
} }
/* ==========================================================================================
* Disposable
* ========================================================================================== */
private val uiDisposables = CompositeDisposable()
protected fun Disposable.disposeOnDestroyView(): Disposable {
uiDisposables.add(this)
return this
}
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
observer(it)
}
.disposeOnDestroyView()
}
} }

View File

@ -31,7 +31,11 @@ import com.google.android.material.chip.ChipGroup
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.* import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import kotlinx.android.synthetic.main.fragment_create_direct_room.* import kotlinx.android.synthetic.main.fragment_create_direct_room.*
@ -57,8 +61,10 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
setupFilterView() setupFilterView()
setupAddByMatrixIdView() setupAddByMatrixIdView()
setupCloseView() setupCloseView()
viewModel.selectUserEvent.observeEvent(this) { viewModel.observeViewEvents {
updateChipsView(it) when (it) {
is CreateDirectRoomViewEvents.SelectUserAction -> updateChipsView(it)
}.exhaustive
} }
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) { viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
renderSelectedUsers(it) renderSelectedUsers(it)
@ -132,7 +138,7 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
knownUsersController.setData(it) knownUsersController.setData(it)
} }
private fun updateChipsView(data: SelectUserAction) { private fun updateChipsView(data: CreateDirectRoomViewEvents.SelectUserAction) {
if (data.isAdded) { if (data.isAdded) {
addChipToGroup(data.user, chipGroup) addChipToGroup(data.user, chipGroup)
} else { } else {

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 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.riotx.features.createdirect
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for create direct room screen
*/
sealed class CreateDirectRoomViewEvents : VectorViewEvents {
data class SelectUserAction(
val user: User,
val isAdded: Boolean,
val index: Int
) : CreateDirectRoomViewEvents()
}

View File

@ -18,8 +18,6 @@
package im.vector.riotx.features.createdirect package im.vector.riotx.features.createdirect
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
@ -32,10 +30,7 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -43,16 +38,10 @@ import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String private typealias DirectoryUsersSearch = String
data class SelectUserAction(
val user: User,
val isAdded: Boolean,
val index: Int
)
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState, initialState: CreateDirectRoomViewState,
private val session: Session) private val session: Session)
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, EmptyViewEvents>(initialState) { : VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -62,10 +51,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty()) private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>() private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
private val _selectUserEvent = MutableLiveData<LiveEvent<SelectUserAction>>()
val selectUserEvent: LiveData<LiveEvent<SelectUserAction>>
get() = _selectUserEvent
companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> { companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@JvmStatic @JvmStatic
@ -109,7 +94,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId } val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
val selectedUsers = state.selectedUsers.minus(action.user) val selectedUsers = state.selectedUsers.minus(action.user)
setState { copy(selectedUsers = selectedUsers) } setState { copy(selectedUsers = selectedUsers) }
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, false, index)) _viewEvents.post(CreateDirectRoomViewEvents.SelectUserAction(action.user, false, index))
} }
private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state -> private fun handleSelectUser(action: CreateDirectRoomAction.SelectUser) = withState { state ->
@ -129,7 +114,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
isAddOperation = false isAddOperation = false
} }
setState { copy(selectedUsers = selectedUsers) } setState { copy(selectedUsers = selectedUsers) }
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex)) _viewEvents.post(CreateDirectRoomViewEvents.SelectUserAction(action.user, isAddOperation, changeIndex))
} }
private fun observeDirectoryUsers() { private fun observeDirectoryUsers() {

View File

@ -23,18 +23,17 @@ import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.transition.AutoTransition import androidx.transition.AutoTransition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import butterknife.BindView import butterknife.BindView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
@ -56,7 +55,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
val verificationId: String? = null, val verificationId: String? = null,
val roomId: String? = null, val roomId: String? = null,
// Special mode where UX should show loading wheel until other user sends a request/tx // Special mode where UX should show loading wheel until other user sends a request/tx
val waitForIncomingRequest : Boolean = false val waitForIncomingRequest: Boolean = false
) : Parcelable ) : Parcelable
@Inject @Inject
@ -84,17 +83,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.requestLiveData.observe(viewLifecycleOwner, Observer { viewModel.observeViewEvents {
it.peekContent().let { va -> when (it) {
when (va) { is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
is Success -> { }.exhaustive
if (va.invoke() is VerificationAction.GotItConclusion) { }
dismiss()
}
}
}
}
})
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
@ -250,7 +243,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
} }
val WAITING_SELF_VERIF_TAG : String = "WAITING_SELF_VERIF_TAG" const val WAITING_SELF_VERIF_TAG: String = "WAITING_SELF_VERIF_TAG"
} }
} }

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 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.riotx.features.crypto.verification
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for the verification bottom sheet
*/
sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents()
}

View File

@ -15,8 +15,6 @@
*/ */
package im.vector.riotx.features.crypto.verification package im.vector.riotx.features.crypto.verification
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
@ -43,9 +41,7 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
data class VerificationBottomSheetViewState( data class VerificationBottomSheetViewState(
val otherUserMxItem: MatrixItem? = null, val otherUserMxItem: MatrixItem? = null,
@ -63,14 +59,9 @@ data class VerificationBottomSheetViewState(
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState, class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
@Assisted args: VerificationBottomSheet.VerificationArgs, @Assisted args: VerificationBottomSheet.VerificationArgs,
private val session: Session) private val session: Session)
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction, EmptyViewEvents>(initialState), : VectorViewModel<VerificationBottomSheetViewState, VerificationAction, VerificationBottomSheetViewEvents>(initialState),
VerificationService.VerificationListener { VerificationService.VerificationListener {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
get() = _requestLiveData
init { init {
session.getVerificationService().addListener(this) session.getVerificationService().addListener(this)
@ -255,7 +246,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
?.shortCodeDoesNotMatch() ?.shortCodeDoesNotMatch()
} }
is VerificationAction.GotItConclusion -> { is VerificationAction.GotItConclusion -> {
_requestLiveData.postValue(LiveEvent(Success(action))) _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
} }
}.exhaustive }.exhaustive
} }

View File

@ -26,7 +26,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.HomeActivitySharedAction import im.vector.riotx.features.home.HomeActivitySharedAction
@ -51,8 +51,10 @@ class GroupListFragment @Inject constructor(
stateView.contentView = groupListView stateView.contentView = groupListView
groupListView.configureWith(groupController) groupListView.configureWith(groupController)
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) { viewModel.observeViewEvents {
sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) when (it) {
is GroupListViewEvents.OpenGroupSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
}.exhaustive
} }
} }

View File

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,12 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.grouplist
import java.io.File import im.vector.riotx.core.platform.VectorViewEvents
data class DownloadFileState( /**
val mimeType: String, * Transient events for group list screen
val file: File?, */
val throwable: Throwable? sealed class GroupListViewEvents : VectorViewEvents {
) object OpenGroupSummary : GroupListViewEvents()
}

View File

@ -17,8 +17,6 @@
package im.vector.riotx.features.grouplist package im.vector.riotx.features.grouplist
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
@ -32,11 +30,8 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
@ -46,7 +41,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private val selectedGroupStore: SelectedGroupDataSource, private val selectedGroupStore: SelectedGroupDataSource,
private val session: Session, private val session: Session,
private val stringProvider: StringProvider private val stringProvider: StringProvider
) : VectorViewModel<GroupListViewState, GroupListAction, EmptyViewEvents>(initialState) { ) : VectorViewModel<GroupListViewState, GroupListAction, GroupListViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -62,9 +57,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
} }
} }
private val _openGroupLiveData = MutableLiveData<LiveEvent<GroupSummary>>() private var currentGroupId = ""
val openGroupLiveData: LiveData<LiveEvent<GroupSummary>>
get() = _openGroupLiveData
init { init {
observeGroupSummaries() observeGroupSummaries()
@ -74,10 +67,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun observeSelectionState() { private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) { groupSummary -> selectSubscribe(GroupListViewState::selectedGroup) { groupSummary ->
if (groupSummary != null) { if (groupSummary != null) {
val selectedGroup = _openGroupLiveData.value?.peekContent()
// We only want to open group if the updated selectedGroup is a different one. // We only want to open group if the updated selectedGroup is a different one.
if (selectedGroup?.groupId != groupSummary.groupId) { if (currentGroupId != groupSummary.groupId) {
_openGroupLiveData.postLiveEvent(groupSummary) currentGroupId = groupSummary.groupId
_viewEvents.post(GroupListViewEvents.OpenGroupSummary)
} }
val optionGroup = Option.just(groupSummary) val optionGroup = Option.just(groupSummary)
selectedGroupStore.post(optionGroup) selectedGroupStore.post(optionGroup)

View File

@ -90,7 +90,6 @@ import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
@ -254,12 +253,7 @@ class RoomDetailFragment @Inject constructor(
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
} }
roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.subscribe { renderState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) }
roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair ->
val message = getString(pair.first, *pair.second.toTypedArray())
showSnackWithMessage(message, Snackbar.LENGTH_LONG)
}
sharedActionViewModel sharedActionViewModel
.observe() .observe()
.subscribe { .subscribe {
@ -267,34 +261,10 @@ class RoomDetailFragment @Inject constructor(
} }
.disposeOnDestroyView() .disposeOnDestroyView()
roomDetailViewModel.navigateToEvent.observeEvent(this) {
val scrollPosition = timelineEventController.searchPositionOfEvent(it)
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(it)
} else {
recyclerView.stopScroll()
layoutManager.scrollToPosition(scrollPosition)
}
}
roomDetailViewModel.fileTooBigEvent.observeEvent(this) {
displayFileTooBigWarning(it)
}
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
renderTombstoneEventHandling(it) renderTombstoneEventHandling(it)
} }
roomDetailViewModel.downloadedFileEvent.observeEvent(this) { downloadFileState ->
val activity = requireActivity()
if (downloadFileState.throwable != null) {
activity.toast(errorFormatter.toHumanReadable(downloadFileState.throwable))
} else if (downloadFileState.file != null) {
activity.toast(getString(R.string.downloaded_file, downloadFileState.file.path))
addEntryToDownloadManager(activity, downloadFileState.file, downloadFileState.mimeType)
}
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode -> roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
when (mode) { when (mode) {
is SendMode.REGULAR -> renderRegularMode(mode.text) is SendMode.REGULAR -> renderRegularMode(mode.text)
@ -308,14 +278,17 @@ class RoomDetailFragment @Inject constructor(
syncStateView.render(syncState) syncStateView.render(syncState)
} }
roomDetailViewModel.requestLiveData.observeEvent(this) {
displayRoomDetailActionResult(it)
}
roomDetailViewModel.observeViewEvents { roomDetailViewModel.observeViewEvents {
when (it) { when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable) is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds) is RoomDetailViewEvents.OnNewTimelineEvents -> scrollOnNewMessageCallback.addNewTimelineEventIds(it.eventIds)
is RoomDetailViewEvents.ActionSuccess -> displayRoomDetailActionSuccess(it)
is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it)
is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message, Snackbar.LENGTH_LONG)
is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it)
is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it)
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
}.exhaustive }.exhaustive
} }
} }
@ -370,18 +343,38 @@ class RoomDetailFragment @Inject constructor(
jumpToReadMarkerView.callback = this jumpToReadMarkerView.callback = this
} }
private fun displayFileTooBigWarning(error: FileTooBigError) { private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
val scrollPosition = timelineEventController.searchPositionOfEvent(action.eventId)
if (scrollPosition == null) {
scrollOnHighlightedEventCallback.scheduleScrollTo(action.eventId)
} else {
recyclerView.stopScroll()
layoutManager.scrollToPosition(scrollPosition)
}
}
private fun displayFileTooBigError(action: RoomDetailViewEvents.FileTooBigError) {
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.error_file_too_big, .setMessage(getString(R.string.error_file_too_big,
error.filename, action.filename,
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), TextUtils.formatFileSize(requireContext(), action.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) TextUtils.formatFileSize(requireContext(), action.homeServerLimitInBytes)
)) ))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
private fun handleDownloadFileState(action: RoomDetailViewEvents.DownloadFileState) {
val activity = requireActivity()
if (action.throwable != null) {
activity.toast(errorFormatter.toHumanReadable(action.throwable))
} else if (action.file != null) {
activity.toast(getString(R.string.downloaded_file, action.file.path))
addEntryToDownloadManager(activity, action.file, action.mimeType)
}
}
private fun setupNotificationView() { private fun setupNotificationView() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate { notificationAreaView.delegate = object : NotificationAreaView.Delegate {
override fun onTombstoneEventClicked(tombstoneEvent: Event) { override fun onTombstoneEventClicked(tombstoneEvent: Event) {
@ -740,31 +733,31 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) {
when (sendMessageResult) { when (sendMessageResult) {
is SendMessageResult.MessageSent -> { is RoomDetailViewEvents.MessageSent -> {
updateComposerText("") updateComposerText("")
} }
is SendMessageResult.SlashCommandHandled -> { is RoomDetailViewEvents.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
updateComposerText("") updateComposerText("")
} }
is SendMessageResult.SlashCommandError -> { is RoomDetailViewEvents.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
} }
is SendMessageResult.SlashCommandUnknown -> { is RoomDetailViewEvents.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
} }
is SendMessageResult.SlashCommandResultOk -> { is RoomDetailViewEvents.SlashCommandResultOk -> {
updateComposerText("") updateComposerText("")
} }
is SendMessageResult.SlashCommandResultError -> { is RoomDetailViewEvents.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage) displayCommandError(sendMessageResult.throwable.localizedMessage)
} }
is SendMessageResult.SlashCommandNotImplemented -> { is RoomDetailViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented)) displayCommandError(getString(R.string.not_implemented))
} }
} } // .exhaustive
lockSendButton = false lockSendButton = false
} }
@ -814,84 +807,81 @@ class RoomDetailFragment @Inject constructor(
.show() .show()
} }
private fun displayRoomDetailActionResult(result: Async<RoomDetailAction>) { private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
when (result) { AlertDialog.Builder(requireActivity())
is Fail -> { .setTitle(R.string.dialog_title_error)
AlertDialog.Builder(requireActivity()) .setMessage(errorFormatter.toHumanReadable(result.throwable))
.setTitle(R.string.dialog_title_error) .setPositiveButton(R.string.ok, null)
.setMessage(errorFormatter.toHumanReadable(result.error)) .show()
.setPositiveButton(R.string.ok, null) }
.show()
} private fun displayRoomDetailActionSuccess(result: RoomDetailViewEvents.ActionSuccess) {
is Success -> { when (val data = result.action) {
when (val data = result.invoke()) { is RoomDetailAction.ReportContent -> {
is RoomDetailAction.ReportContent -> { when {
when { data.spam -> {
data.spam -> { AlertDialog.Builder(requireActivity())
AlertDialog.Builder(requireActivity()) .setTitle(R.string.content_reported_as_spam_title)
.setTitle(R.string.content_reported_as_spam_title) .setMessage(R.string.content_reported_as_spam_content)
.setMessage(R.string.content_reported_as_spam_content) .setPositiveButton(R.string.ok, null)
.setPositiveButton(R.string.ok, null) .setNegativeButton(R.string.block_user) { _, _ ->
.setNegativeButton(R.string.block_user) { _, _ -> roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId)) }
} .show()
.show() .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
data.inappropriate -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_inappropriate_title)
.setMessage(R.string.content_reported_as_inappropriate_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
else -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
}
} }
is RoomDetailAction.RequestVerification -> { data.inappropriate -> {
Timber.v("## SAS RequestVerification action") AlertDialog.Builder(requireActivity())
VerificationBottomSheet.withArgs( .setTitle(R.string.content_reported_as_inappropriate_title)
roomDetailArgs.roomId, .setMessage(R.string.content_reported_as_inappropriate_content)
data.userId .setPositiveButton(R.string.ok, null)
).show(parentFragmentManager, "REQ") .setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
} }
is RoomDetailAction.AcceptVerificationRequest -> { else -> {
Timber.v("## SAS AcceptVerificationRequest action") AlertDialog.Builder(requireActivity())
VerificationBottomSheet.withArgs( .setTitle(R.string.content_reported_title)
roomDetailArgs.roomId, .setMessage(R.string.content_reported_content)
data.otherUserId, .setPositiveButton(R.string.ok, null)
data.transactionId .setNegativeButton(R.string.block_user) { _, _ ->
).show(parentFragmentManager, "REQ") roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
} }
is RoomDetailAction.ResumeVerification -> { .show()
val otherUserId = data.otherUserId ?: return .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
otherUserId, data.transactionId, roomId = roomDetailArgs.roomId))
}
}.show(parentFragmentManager, "REQ")
} }
} }
} }
is RoomDetailAction.RequestVerification -> {
Timber.v("## SAS RequestVerification action")
VerificationBottomSheet.withArgs(
roomDetailArgs.roomId,
data.userId
).show(parentFragmentManager, "REQ")
}
is RoomDetailAction.AcceptVerificationRequest -> {
Timber.v("## SAS AcceptVerificationRequest action")
VerificationBottomSheet.withArgs(
roomDetailArgs.roomId,
data.otherUserId,
data.transactionId
).show(parentFragmentManager, "REQ")
}
is RoomDetailAction.ResumeVerification -> {
val otherUserId = data.otherUserId ?: return
VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationBottomSheet.VerificationArgs(
otherUserId, data.transactionId, roomId = roomDetailArgs.roomId))
}
}.show(parentFragmentManager, "REQ")
}
} }
} }
// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String): Boolean { override fun onUrlClicked(url: String): Boolean {
permalinkHandler permalinkHandler

View File

@ -16,7 +16,10 @@
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.home.room.detail
import androidx.annotation.StringRes
import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.features.command.Command
import java.io.File
/** /**
* Transient events for RoomDetail * Transient events for RoomDetail
@ -24,4 +27,34 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class RoomDetailViewEvents : VectorViewEvents { sealed class RoomDetailViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomDetailViewEvents() data class Failure(val throwable: Throwable) : 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 ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents()
data class ShowMessage(val message: String) : RoomDetailViewEvents()
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class FileTooBigError(
val filename: String,
val fileSizeInBytes: Long,
val homeServerLimitInBytes: Long
) : RoomDetailViewEvents()
data class DownloadFileState(
val mimeType: String,
val file: File?,
val throwable: Throwable?
) : RoomDetailViewEvents()
abstract class SendMessageResult : RoomDetailViewEvents()
object MessageSent : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult()
class SlashCommandUnknown(val command: String) : SendMessageResult()
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
object SlashCommandResultOk : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
// TODO Remove
object SlashCommandNotImplemented : SendMessageResult()
} }

View File

@ -18,10 +18,6 @@ package im.vector.riotx.features.home.room.detail
import android.net.Uri import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
@ -32,6 +28,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -61,17 +58,14 @@ import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.LiveEvent
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.crypto.verification.supportedVerificationMethods import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.home.room.typing.TypingHelper import im.vector.riotx.features.home.room.typing.TypingHelper
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
@ -116,11 +110,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
var timeline = room.createTimeline(eventId, timelineSettings) var timeline = room.createTimeline(eventId, timelineSettings)
private set private set
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<RoomDetailAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<RoomDetailAction>>>
get() = _requestLiveData
// Slot to keep a pending action during permission request // Slot to keep a pending action during permission request
var pendingAction: RoomDetailAction? = null var pendingAction: RoomDetailAction? = null
// Slot to keep a pending uri during permission request // Slot to keep a pending uri during permission request
@ -320,27 +309,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
// TODO Cleanup this and use ViewEvents
private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
get() = _nonBlockingPopAlert
private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
get() = _sendMessageResultLiveData
private val _navigateToEvent = MutableLiveData<LiveEvent<String>>()
val navigateToEvent: LiveData<LiveEvent<String>>
get() = _navigateToEvent
private val _fileTooBigEvent = MutableLiveData<LiveEvent<FileTooBigError>>()
val fileTooBigEvent: LiveData<LiveEvent<FileTooBigError>>
get() = _fileTooBigEvent
private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>()
val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>>
get() = _downloadedFileEvent
fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) { fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
R.id.clear_message_queue -> R.id.clear_message_queue ->
/* For now always disable on production, worker cancellation is not working properly */ /* For now always disable on production, worker cancellation is not working properly */
@ -360,17 +328,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown) room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
is ParsedCommand.ErrorSyntax -> { is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)) _viewEvents.post(RoomDetailViewEvents.SlashCommandError(slashCommandResult.command))
} }
is ParsedCommand.ErrorEmptySlashCommand -> { is ParsedCommand.ErrorEmptySlashCommand -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown("/")) _viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown("/"))
} }
is ParsedCommand.ErrorUnknownSlashCommand -> { is ParsedCommand.ErrorUnknownSlashCommand -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)) _viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
} }
is ParsedCommand.Invite -> { is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult) handleInviteSlashCommand(slashCommandResult)
@ -378,55 +346,55 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
is ParsedCommand.SetUserPowerLevel -> { is ParsedCommand.SetUserPowerLevel -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.ClearScalarToken -> { is ParsedCommand.ClearScalarToken -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.SetMarkdown -> { is ParsedCommand.SetMarkdown -> {
vectorPreferences.setMarkdownEnabled(slashCommandResult.enable) vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled( _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled(
if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled)) if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
popDraft() popDraft()
} }
is ParsedCommand.UnbanUser -> { is ParsedCommand.UnbanUser -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.BanUser -> { is ParsedCommand.BanUser -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.KickUser -> { is ParsedCommand.KickUser -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.JoinRoom -> { is ParsedCommand.JoinRoom -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.PartRoom -> { is ParsedCommand.PartRoom -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.SendEmote -> { is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.SendRainbow -> { is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let { slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it)) room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
} }
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.SendRainbowEmote -> { is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let { slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE) room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
} }
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.SendSpoiler -> { is ParsedCommand.SendSpoiler -> {
@ -434,7 +402,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})", "[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
"<span data-mx-spoiler>${slashCommandResult.message}</span>" "<span data-mx-spoiler>${slashCommandResult.message}</span>"
) )
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.SendShrug -> { is ParsedCommand.SendShrug -> {
@ -446,12 +414,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
room.sendTextMessage(sequence) room.sendTextMessage(sequence)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.VerifyUser -> { is ParsedCommand.VerifyUser -> {
session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId) session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.ChangeTopic -> { is ParsedCommand.ChangeTopic -> {
@ -460,7 +428,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
is ParsedCommand.ChangeDisplayName -> { is ParsedCommand.ChangeDisplayName -> {
// TODO // TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
}.exhaustive }.exhaustive
} }
@ -487,7 +455,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
} }
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
is SendMode.QUOTE -> { is SendMode.QUOTE -> {
@ -510,13 +478,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else { } else {
room.sendFormattedTextMessage(finalText, htmlText) room.sendFormattedTextMessage(finalText, htmlText)
} }
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
is SendMode.REPLY -> { is SendMode.REPLY -> {
state.sendMode.timelineEvent.let { state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown) room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
} }
@ -549,29 +517,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
room.updateTopic(changeTopic.topic, object : MatrixCallback<Unit> { room.updateTopic(changeTopic.topic, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) _viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure)) _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
} }
}) })
} }
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
room.invite(invite.userId, invite.reason, object : MatrixCallback<Unit> { room.invite(invite.userId, invite.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) _viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure)) _viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
} }
}) })
} }
@ -608,8 +576,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else { } else {
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments) null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) tooBigFile.name ?: tooBigFile.path,
tooBigFile.size,
maxUploadFileSize
))
} }
} }
} }
@ -721,7 +692,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(), action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> { object : MatrixCallback<File> {
override fun onSuccess(data: File) { override fun onSuccess(data: File) {
_downloadedFileEvent.postLiveEvent(DownloadFileState( _viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.getMimeType(), action.messageFileContent.getMimeType(),
data, data,
null null
@ -729,7 +700,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
_downloadedFileEvent.postLiveEvent(DownloadFileState( _viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.getMimeType(), action.messageFileContent.getMimeType(),
null, null,
failure failure
@ -750,7 +721,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
if (action.highlight) { if (action.highlight) {
setState { copy(highlightedEventId = correctedEventId) } setState { copy(highlightedEventId = correctedEventId) }
} }
_navigateToEvent.postLiveEvent(correctedEventId) _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(correctedEventId))
} }
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
@ -821,11 +792,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleReportContent(action: RoomDetailAction.ReportContent) { private fun handleReportContent(action: RoomDetailAction.ReportContent) {
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> { room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action))) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
_requestLiveData.postValue(LiveEvent(Fail(failure))) _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
} }
}) })
} }
@ -837,11 +808,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
session.ignoreUserIds(listOf(action.userId), object : MatrixCallback<Unit> { session.ignoreUserIds(listOf(action.userId), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action))) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
_requestLiveData.postValue(LiveEvent(Fail(failure))) _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
} }
}) })
} }
@ -853,7 +824,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
action.otherUserId, action.otherUserId,
room.roomId, room.roomId,
action.transactionId)) { action.transactionId)) {
_requestLiveData.postValue(LiveEvent(Success(action))) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} else { } else {
// TODO // TODO
} }
@ -869,7 +840,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) { private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
if (action.userId == session.myUserId) return if (action.userId == session.myUserId) return
_requestLiveData.postValue(LiveEvent(Success(action))) _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} }
private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) { private fun handleResumeRequestVerification(action: RoomDetailAction.ResumeVerification) {
@ -877,9 +848,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
session.getVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let { session.getVerificationService().getExistingVerificationRequestInRoom(room.roomId, action.transactionId)?.let {
if (it.handledByOtherSession) return if (it.handledByOtherSession) return
if (!it.isFinished) { if (!it.isFinished) {
_requestLiveData.postValue(LiveEvent(Success(action.copy( _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action.copy(
otherUserId = it.otherUserId otherUserId = it.otherUserId
)))) )))
} }
} }
} }

View File

@ -1,31 +0,0 @@
/*
* 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.riotx.features.home.room.detail
import androidx.annotation.StringRes
import im.vector.riotx.features.command.Command
sealed class SendMessageResult {
object MessageSent : SendMessageResult()
class SlashCommandError(val command: Command) : SendMessageResult()
class SlashCommandUnknown(val command: String) : SendMessageResult()
data class SlashCommandHandled(@StringRes val messageRes: Int? = null) : SendMessageResult()
object SlashCommandResultOk : SendMessageResult()
class SlashCommandResultError(val throwable: Throwable) : SendMessageResult()
// TODO Remove
object SlashCommandNotImplemented : SendMessageResult()
}

View File

@ -28,7 +28,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.* import kotlinx.android.synthetic.main.fragment_public_rooms.*
@ -75,13 +75,20 @@ class PublicRoomsFragment @Inject constructor(
sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoom) sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoom)
} }
// TODO remove this, replace by ViewEvents viewModel.observeViewEvents {
viewModel.joinRoomErrorLiveData.observeEvent(this) { throwable -> handleViewEvents(it)
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
.show()
} }
} }
private fun handleViewEvents(viewEvents: RoomDirectoryViewEvents) {
when (viewEvents) {
is RoomDirectoryViewEvents.Failure -> {
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(viewEvents.throwable), Snackbar.LENGTH_SHORT)
.show()
}
}.exhaustive
}
override fun onDestroyView() { override fun onDestroyView() {
publicRoomsController.callback = null publicRoomsController.callback = null
publicRoomsList.cleanup() publicRoomsList.cleanup()

View File

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,10 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home.room.detail package im.vector.riotx.features.roomdirectory
data class FileTooBigError( import im.vector.riotx.core.platform.VectorViewEvents
val filename: String,
val fileSizeInBytes: Long, /**
val homeServerLimitInBytes: Long * Transient events for room directory screen
) */
sealed class RoomDirectoryViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomDirectoryViewEvents()
}

View File

@ -16,9 +16,13 @@
package im.vector.riotx.features.roomdirectory package im.vector.riotx.features.roomdirectory
import androidx.lifecycle.LiveData import com.airbnb.mvrx.ActivityViewModelContext
import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.* import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.appendAt
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
@ -32,17 +36,14 @@ import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryD
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber import timber.log.Timber
private const val PUBLIC_ROOMS_LIMIT = 20 private const val PUBLIC_ROOMS_LIMIT = 20
class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState, class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState,
private val session: Session) private val session: Session)
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, EmptyViewEvents>(initialState) { : VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, RoomDirectoryViewEvents>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -58,10 +59,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
} }
} }
private val _joinRoomErrorLiveData = MutableLiveData<LiveEvent<Throwable>>()
val joinRoomErrorLiveData: LiveData<LiveEvent<Throwable>>
get() = _joinRoomErrorLiveData
private var since: String? = null private var since: String? = null
private var currentTask: Cancelable? = null private var currentTask: Cancelable? = null
@ -109,9 +106,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomDirectoryAction) { override fun handle(action: RoomDirectoryAction) {
when (action) { when (action) {
is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action) is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
is RoomDirectoryAction.FilterWith -> filterWith(action) is RoomDirectoryAction.FilterWith -> filterWith(action)
RoomDirectoryAction.LoadMore -> loadMore() RoomDirectoryAction.LoadMore -> loadMore()
is RoomDirectoryAction.JoinRoom -> joinRoom(action) is RoomDirectoryAction.JoinRoom -> joinRoom(action)
} }
} }
@ -227,7 +224,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// Notify the user // Notify the user
_joinRoomErrorLiveData.postLiveEvent(failure) _viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
setState { setState {
copy( copy(

View File

@ -20,8 +20,7 @@ package im.vector.riotx.features.roommemberprofile
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomMemberProfileAction : VectorViewModelAction { sealed class RoomMemberProfileAction : VectorViewModelAction {
object RetryFetchingInfo : RoomMemberProfileAction()
object RetryFetchingInfo: RoomMemberProfileAction() object IgnoreUser : RoomMemberProfileAction()
object IgnoreUser: RoomMemberProfileAction() object VerifyUser : RoomMemberProfileAction()
data class VerifyUser(val userId: String? = null, val roomId: String? = null, val canCrossSign: Boolean? = true): RoomMemberProfileAction()
} }

View File

@ -35,7 +35,6 @@ import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
@ -94,33 +93,27 @@ class RoomMemberProfileFragment @Inject constructor(
is RoomMemberProfileViewEvents.Loading -> showLoading(it.message) is RoomMemberProfileViewEvents.Loading -> showLoading(it.message)
is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable) is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it)
}.exhaustive }.exhaustive
} }
viewModel.actionResultLiveData.observeEvent(this) { async -> }
when (async) {
is Success -> { private fun handleStartVerification(startVerification: RoomMemberProfileViewEvents.StartVerification) {
when (val action = async.invoke()) { if (startVerification.canCrossSign) {
is RoomMemberProfileAction.VerifyUser -> { VerificationBottomSheet
if (action.canCrossSign == true) { .withArgs(roomId = null, otherUserId = startVerification.userId)
VerificationBottomSheet .show(parentFragmentManager, "VERIF")
.withArgs(roomId = null, otherUserId = action.userId!!) } else {
.show(parentFragmentManager, "VERIF") AlertDialog.Builder(requireContext())
} else { .setTitle(R.string.dialog_title_warning)
AlertDialog.Builder(requireContext()) .setMessage(R.string.verify_cannot_cross_sign)
.setTitle(R.string.dialog_title_warning) .setPositiveButton(R.string.verification_profile_verify) { _, _ ->
.setMessage(R.string.verify_cannot_cross_sign) VerificationBottomSheet
.setPositiveButton(R.string.verification_profile_verify) { _, _ -> .withArgs(roomId = null, otherUserId = startVerification.userId)
VerificationBottomSheet .show(parentFragmentManager, "VERIF")
.withArgs(roomId = null, otherUserId = action.userId!!)
.show(parentFragmentManager, "VERIF")
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
} }
} .setNegativeButton(R.string.cancel, null)
} .show()
} }
} }
@ -197,7 +190,7 @@ class RoomMemberProfileFragment @Inject constructor(
} }
override fun onTapVerify() { override fun onTapVerify() {
viewModel.handle(RoomMemberProfileAction.VerifyUser()) viewModel.handle(RoomMemberProfileAction.VerifyUser)
} }
override fun onShowDeviceList() = withState(viewModel) { override fun onShowDeviceList() = withState(viewModel) {

View File

@ -26,4 +26,9 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents() data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents()
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents() object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
data class StartVerification(
val userId: String,
val canCrossSign: Boolean
) : RoomMemberProfileViewEvents()
} }

View File

@ -17,10 +17,7 @@
package im.vector.riotx.features.roommemberprofile package im.vector.riotx.features.roommemberprofile
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
@ -49,7 +46,6 @@ import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -75,10 +71,6 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
} }
} }
private val _actionResultLiveData = MutableLiveData<LiveEvent<Async<RoomMemberProfileAction>>>()
val actionResultLiveData: LiveData<LiveEvent<Async<RoomMemberProfileAction>>>
get() = _actionResultLiveData
private val room = if (initialState.roomId != null) { private val room = if (initialState.roomId != null) {
session.getRoom(initialState.roomId) session.getRoom(initialState.roomId)
} else { } else {
@ -145,23 +137,19 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
when (action) { when (action) {
is RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo() is RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo()
is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction() is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction()
is RoomMemberProfileAction.VerifyUser -> prepareVerification(action) is RoomMemberProfileAction.VerifyUser -> prepareVerification()
} }
} }
private fun prepareVerification(action: RoomMemberProfileAction.VerifyUser) = withState { state -> private fun prepareVerification() = withState { state ->
// Sanity // Sanity
if (state.isRoomEncrypted) { if (state.isRoomEncrypted) {
if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) { if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) {
// ok, let's find or create the DM room // ok, let's find or create the DM room
_actionResultLiveData.postValue( _viewEvents.post(RoomMemberProfileViewEvents.StartVerification(
LiveEvent(Success( userId = state.userId,
action.copy( canCrossSign = session.getCrossSigningService().canCrossSign()
userId = state.userId, ))
canCrossSign = session.getCrossSigningService().canCrossSign()
)
))
)
} }
} }
} }

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 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.riotx.features.roommemberprofile.devices
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DeviceListAction : VectorViewModelAction {
data class SelectDevice(val device: CryptoDeviceInfo) : DeviceListAction()
object DeselectDevice : DeviceListAction()
data class ManuallyVerify(val deviceId: String) : DeviceListAction()
}

View File

@ -21,15 +21,13 @@ import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.crypto.verification.VerificationAction
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -48,20 +46,16 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
viewModel.requestLiveData.observeEvent(this) { async -> viewModel.observeViewEvents {
when (async) { when (it) {
is Success -> { is DeviceListBottomSheetViewEvents.Verify -> {
when (val action = async.invoke()) { VerificationBottomSheet.withArgs(
is VerificationAction.StartSASVerification -> { roomId = null,
VerificationBottomSheet.withArgs( otherUserId = it.userId,
roomId = null, transactionId = it.txID
otherUserId = action.otherUserId, ).show(requireActivity().supportFragmentManager, "REQPOP")
transactionId = action.pendingRequestTransactionId
).show(requireActivity().supportFragmentManager, "REQPOP")
}
}
} }
} }.exhaustive
} }
} }
@ -69,7 +63,7 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
withState(viewModel) { withState(viewModel) {
if (keyCode == KeyEvent.KEYCODE_BACK) { if (keyCode == KeyEvent.KEYCODE_BACK) {
if (it.selectedDevice != null) { if (it.selectedDevice != null) {
viewModel.selectDevice(null) viewModel.handle(DeviceListAction.DeselectDevice)
return@withState true return@withState true
} else { } else {
return@withState false return@withState false

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 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.riotx.features.roommemberprofile.devices
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for device list screen
*/
sealed class DeviceListBottomSheetViewEvents : VectorViewEvents {
data class Verify(val userId: String, val txID: String) : DeviceListBottomSheetViewEvents()
}

View File

@ -16,14 +16,11 @@
*/ */
package im.vector.riotx.features.roommemberprofile.devices package im.vector.riotx.features.roommemberprofile.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -35,12 +32,8 @@ import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyAction import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.VerificationAction
data class DeviceListViewState( data class DeviceListViewState(
val userItem: MatrixItem? = null, val userItem: MatrixItem? = null,
@ -52,14 +45,8 @@ data class DeviceListViewState(
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState, class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
@Assisted private val userId: String, @Assisted private val userId: String,
private val stringProvider: StringProvider,
private val session: Session) private val session: Session)
: VectorViewModel<DeviceListViewState, EmptyAction, EmptyViewEvents>(initialState) { : VectorViewModel<DeviceListViewState, DeviceListAction, DeviceListBottomSheetViewEvents>(initialState) {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<VerificationAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<VerificationAction>>>
get() = _requestLiveData
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -67,7 +54,6 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
} }
init { init {
session.rx().liveUserCryptoDevices(userId) session.rx().liveUserCryptoDevices(userId)
.execute { .execute {
copy(cryptoDevices = it).also { copy(cryptoDevices = it).also {
@ -81,6 +67,14 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
} }
} }
override fun handle(action: DeviceListAction) {
when (action) {
is DeviceListAction.SelectDevice -> selectDevice(action)
is DeviceListAction.DeselectDevice -> deselectDevice()
is DeviceListAction.ManuallyVerify -> manuallyVerify(action)
}.exhaustive
}
private fun refreshSelectedId() = withState { state -> private fun refreshSelectedId() = withState { state ->
if (state.selectedDevice != null) { if (state.selectedDevice != null) {
state.cryptoDevices.invoke()?.firstOrNull { state.selectedDevice.deviceId == it.deviceId }?.let { state.cryptoDevices.invoke()?.firstOrNull { state.selectedDevice.deviceId == it.deviceId }?.let {
@ -93,19 +87,23 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
} }
} }
fun selectDevice(device: CryptoDeviceInfo?) { private fun selectDevice(action: DeviceListAction.SelectDevice) {
setState { setState {
copy(selectedDevice = device) copy(selectedDevice = action.device)
} }
} }
fun manuallyVerify(device: CryptoDeviceInfo) { private fun deselectDevice() {
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, device.deviceId, null)?.let { txID -> setState {
_requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID)))) copy(selectedDevice = null)
} }
} }
override fun handle(action: EmptyAction) {} private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) {
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID ->
_viewEvents.post(DeviceListBottomSheetViewEvents.Verify(userId, txID))
}
}
companion object : MvRxViewModelFactory<DeviceListBottomSheetViewModel, DeviceListViewState> { companion object : MvRxViewModelFactory<DeviceListBottomSheetViewModel, DeviceListViewState> {
@JvmStatic @JvmStatic

View File

@ -62,6 +62,6 @@ class DeviceListFragment @Inject constructor(
} }
override fun onDeviceSelected(device: CryptoDeviceInfo) { override fun onDeviceSelected(device: CryptoDeviceInfo) {
viewModel.selectDevice(device) viewModel.handle(DeviceListAction.SelectDevice(device))
} }
} }

View File

@ -62,6 +62,6 @@ class DeviceTrustInfoActionFragment @Inject constructor(
} }
override fun onVerifyManually(device: CryptoDeviceInfo) { override fun onVerifyManually(device: CryptoDeviceInfo) {
viewModel.manuallyVerify(device) viewModel.handle(DeviceListAction.ManuallyVerify(device.deviceId))
} }
} }

View File

@ -18,16 +18,13 @@ package im.vector.riotx.features.settings.crosssigning
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.PromptPasswordDialog import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
@ -44,23 +41,20 @@ class CrossSigningSettingsFragment @Inject constructor(
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
viewModel.requestLiveData.observeEvent(this) { viewModel.observeViewEvents {
when (it) { when (it) {
is Fail -> { is CrossSigningSettingsViewEvents.Failure -> {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(it.error.message) .setMessage(errorFormatter.toHumanReadable(it.throwable))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
Unit
} }
is Success -> { is CrossSigningSettingsViewEvents.RequestPassword -> {
when (val action = it.invoke()) { requestPassword()
is CrossSigningAction.RequestPasswordAuth -> {
requestPassword(action.sessionId)
}
}
} }
} }.exhaustive
} }
} }
@ -89,18 +83,14 @@ class CrossSigningSettingsFragment @Inject constructor(
super.onDestroyView() super.onDestroyView()
} }
private fun requestPassword(sessionId: String) { private fun requestPassword() {
PromptPasswordDialog().show(requireActivity()) { password -> PromptPasswordDialog().show(requireActivity()) { password ->
// TODO sessionId should never get out the ViewModel viewModel.handle(CrossSigningAction.PasswordEntered(password))
viewModel.handle(CrossSigningAction.InitializeCrossSigning(UserPasswordAuth(
session = sessionId,
password = password
)))
} }
} }
override fun onInitializeCrossSigningKeys() { override fun onInitializeCrossSigningKeys() {
viewModel.handle(CrossSigningAction.InitializeCrossSigning()) viewModel.handle(CrossSigningAction.InitializeCrossSigning)
} }
override fun onResetCrossSigningKeys() { override fun onResetCrossSigningKeys() {
@ -108,7 +98,7 @@ class CrossSigningSettingsFragment @Inject constructor(
.setTitle(R.string.dialog_title_confirmation) .setTitle(R.string.dialog_title_confirmation)
.setMessage(R.string.are_you_sure) .setMessage(R.string.are_you_sure)
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
viewModel.handle(CrossSigningAction.InitializeCrossSigning()) viewModel.handle(CrossSigningAction.InitializeCrossSigning)
} }
.show() .show()
} }

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2020 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.riotx.features.settings.crosssigning
import im.vector.riotx.core.platform.VectorViewEvents
/**
* Transient events for cross signing settings screen
*/
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
object RequestPassword : CrossSigningSettingsViewEvents()
}

View File

@ -15,14 +15,9 @@
*/ */
package im.vector.riotx.features.settings.crosssigning package im.vector.riotx.features.settings.crosssigning
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -36,11 +31,9 @@ import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
data class CrossSigningSettingsViewState( data class CrossSigningSettingsViewState(
val crossSigningInfo: MXCrossSigningInfo? = null, val crossSigningInfo: MXCrossSigningInfo? = null,
@ -51,19 +44,13 @@ data class CrossSigningSettingsViewState(
) : MvRxState ) : MvRxState
sealed class CrossSigningAction : VectorViewModelAction { sealed class CrossSigningAction : VectorViewModelAction {
data class InitializeCrossSigning(val auth: UserPasswordAuth? = null) : CrossSigningAction() object InitializeCrossSigning : CrossSigningAction()
data class RequestPasswordAuth(val sessionId: String) : CrossSigningAction() data class PasswordEntered(val password: String) : CrossSigningAction()
} }
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState, class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
private val stringProvider: StringProvider,
private val session: Session) private val session: Session)
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, EmptyViewEvents>(initialState) { : VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, CrossSigningSettingsViewEvents>(initialState) {
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<CrossSigningAction>>>()
val requestLiveData: LiveData<LiveEvent<Async<CrossSigningAction>>>
get() = _requestLiveData
init { init {
session.rx().liveCrossSigningInfo(session.myUserId) session.rx().liveCrossSigningInfo(session.myUserId)
@ -81,6 +68,9 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
} }
} }
// Storage when password is required
private var _pendingSession: String? = null
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
@ -89,26 +79,37 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
override fun handle(action: CrossSigningAction) { override fun handle(action: CrossSigningAction) {
when (action) { when (action) {
is CrossSigningAction.InitializeCrossSigning -> { is CrossSigningAction.InitializeCrossSigning -> {
initializeCrossSigning(action.auth?.copy(user = session.myUserId)) initializeCrossSigning(null)
} }
} is CrossSigningAction.PasswordEntered -> {
initializeCrossSigning(UserPasswordAuth(
session = _pendingSession,
user = session.myUserId,
password = action.password
))
}
}.exhaustive
} }
private fun initializeCrossSigning(auth: UserPasswordAuth?) { private fun initializeCrossSigning(auth: UserPasswordAuth?) {
_pendingSession = null
setState { setState {
copy(isUploadingKeys = true) copy(isUploadingKeys = true)
} }
session.getCrossSigningService().initializeCrossSigning(auth, object : MatrixCallback<Unit> { session.getCrossSigningService().initializeCrossSigning(auth, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
_pendingSession = null
setState { setState {
copy(isUploadingKeys = false) copy(isUploadingKeys = false)
} }
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.OtherServerError _pendingSession = null
&& failure.httpCode == 401
) { if (failure is Failure.OtherServerError && failure.httpCode == 401) {
try { try {
MoshiProvider.providesMoshi() MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java) .adapter(RegistrationFlowResponse::class.java)
@ -118,23 +119,23 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
}?.let { flowResponse -> }?.let { flowResponse ->
// Retry with authentication // Retry with authentication
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) { if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
_requestLiveData.postValue(LiveEvent(Success(CrossSigningAction.RequestPasswordAuth(flowResponse.session ?: "")))) _pendingSession = flowResponse.session ?: ""
_viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword)
return return
} else { } else {
_requestLiveData.postValue(LiveEvent(Fail(Throwable("You cannot do that from mobile"))))
// can't do this from here // can't do this from here
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
setState {
copy(isUploadingKeys = false)
}
return return
} }
} }
} }
when (failure) {
is Failure.ServerError -> { _viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
_requestLiveData.postValue(LiveEvent(Fail(Throwable(failure.error.message))))
}
else -> {
_requestLiveData.postValue(LiveEvent(Fail(failure)))
}
}
setState { setState {
copy(isUploadingKeys = false) copy(isUploadingKeys = false)
} }

View File

@ -16,14 +16,14 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction() object Retry : DevicesAction()
data class Delete(val deviceId: String) : DevicesAction() data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction() data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction()
data class PromptRename(val deviceId: String, val deviceInfo: DeviceInfo? = null) : DevicesAction()
data class VerifyMyDevice(val deviceId: String, val userId: String? = null, val transactionId: String? = null) : DevicesAction() data class PromptRename(val deviceId: String) : DevicesAction()
data class VerifyMyDevice(val deviceId: String) : DevicesAction()
} }

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.core.platform.VectorViewEvents
/** /**
@ -25,4 +26,13 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class DevicesViewEvents : VectorViewEvents { sealed class DevicesViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DevicesViewEvents() data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
data class Failure(val throwable: Throwable) : DevicesViewEvents() data class Failure(val throwable: Throwable) : DevicesViewEvents()
object RequestPassword : DevicesViewEvents()
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()
data class ShowVerifyDevice(
val userId: String,
val transactionId: String?
) : DevicesViewEvents()
} }

View File

@ -16,8 +16,6 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
@ -41,9 +39,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.supportedVerificationMethods import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
data class DevicesViewState( data class DevicesViewState(
@ -76,15 +72,6 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
private var _currentDeviceId: String? = null private var _currentDeviceId: String? = null
private var _currentSession: String? = null private var _currentSession: String? = null
private val _requestPasswordLiveData = MutableLiveData<LiveEvent<Unit>>()
val requestPasswordLiveData: LiveData<LiveEvent<Unit>>
get() = _requestPasswordLiveData
// Used to communicate back from model to fragment
private val _requestLiveData = MutableLiveData<LiveEvent<Async<DevicesAction>>>()
val fragmentActionLiveData: LiveData<LiveEvent<Async<DevicesAction>>>
get() = _requestLiveData
init { init {
refreshDevicesList() refreshDevicesList()
session.getVerificationService().addListener(this) session.getVerificationService().addListener(this)
@ -187,25 +174,21 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
private fun handleVerify(action: DevicesAction.VerifyMyDevice) { private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
val txID = session.getVerificationService().requestKeyVerification(supportedVerificationMethods, session.myUserId, listOf(action.deviceId)) val txID = session.getVerificationService().requestKeyVerification(supportedVerificationMethods, session.myUserId, listOf(action.deviceId))
_requestLiveData.postValue(LiveEvent(Success( _viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
action.copy( session.myUserId,
userId = session.myUserId, txID.transactionId
transactionId = txID.transactionId ))
)
)))
} }
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state -> private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId } val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
if (info == null) { if (info != null) {
_requestLiveData.postValue(LiveEvent(Uninitialized)) _viewEvents.post(DevicesViewEvents.PromptRenameDevice(info))
} else {
_requestLiveData.postValue(LiveEvent(Success(action.copy(deviceInfo = info))))
} }
} }
private fun handleRename(action: DevicesAction.Rename) { private fun handleRename(action: DevicesAction.Rename) {
session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback<Unit> { session.setDeviceName(action.deviceId, action.newName, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
setState { setState {
copy( copy(
@ -261,7 +244,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
) )
} }
_requestPasswordLiveData.postLiveEvent(Unit) _viewEvents.post(DevicesViewEvents.RequestPassword)
} }
} }

View File

@ -23,7 +23,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -32,7 +31,6 @@ import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
@ -64,35 +62,18 @@ class VectorSettingsDevicesFragment @Inject constructor(
recyclerView.configureWith(devicesController, showDivider = true) recyclerView.configureWith(devicesController, showDivider = true)
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message) is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable) is DevicesViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog()
} is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
viewModel.requestPasswordLiveData.observeEvent(this) { is DevicesViewEvents.ShowVerifyDevice -> {
maybeShowDeleteDeviceWithPasswordDialog() VerificationBottomSheet.withArgs(
} roomId = null,
otherUserId = it.userId,
viewModel.fragmentActionLiveData.observeEvent(this) { async -> transactionId = it.transactionId
when (async) { ).show(childFragmentManager, "REQPOP")
is Success -> {
when (val action = async.invoke()) {
is DevicesAction.PromptRename -> {
action.deviceInfo?.let { deviceInfo ->
displayDeviceRenameDialog(deviceInfo)
}
}
is DevicesAction.VerifyMyDevice -> {
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = action.userId!!,
transactionId = action.transactionId!!
).show(childFragmentManager, "REQPOP")
}
}
}
} }
} }.exhaustive
} }
} }
@ -152,7 +133,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
val newName = input.text.toString() val newName = input.text.toString()
viewModel.handle(DevicesAction.Rename(deviceInfo, newName)) viewModel.handle(DevicesAction.Rename(deviceInfo.deviceId!!, newName))
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()