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

View File

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

View File

@ -15,14 +15,22 @@
*/
package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
/**
* Acts like a visitor on Conditions.
* This class as all required context needed to evaluate rules
*/
interface ConditionResolver {
fun resolveEventMatchCondition(event: Event,
condition: EventMatchCondition): Boolean
fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean
fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean
fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean
fun resolveContainsDisplayNameCondition(containsDisplayNameCondition: ContainsDisplayNameCondition) : Boolean
fun resolveRoomMemberCountCondition(event: Event,
condition: RoomMemberCountCondition): 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 timber.log.Timber
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(event, this)
}
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 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 {
return conditionResolver.resolveEventMatchCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveEventMatchCondition(event, this)
}
override fun technicalDescription(): String {

View File

@ -16,25 +16,32 @@
package im.vector.matrix.android.api.pushrules
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
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 {
return conditionResolver.resolveRoomMemberCountCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(event, this)
}
override fun technicalDescription(): String {
return "Room member count is $iz"
}
fun isSatisfied(event: Event, session: RoomService?): Boolean {
// sanity check^
internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean {
// sanity checks
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
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.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 {
return conditionResolver.resolveSenderNotificationPermissionCondition(this)
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(event, this)
}
override fun technicalDescription(): String {

View File

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
* All push rulesets for a user.
*/
@JsonClass(generateAdapter = true)
data class GetPushRulesResponse(
internal data class GetPushRulesResponse(
/**
* 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.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
@JsonClass(generateAdapter = true)
@ -30,13 +34,13 @@ data class PushCondition(
/**
* Required for event_match conditions. The dot- separated field of the event to match.
*/
val key: String? = null,
/**
*Required for event_match conditions.
*/
/**
* Required for event_match conditions.
*/
val pattern: String? = null,
/**
* Required for room_member_count conditions.
* A decimal integer optionally prefixed by one of, ==, <, >, >= or <=.
@ -47,30 +51,35 @@ data class PushCondition(
) {
fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(this.kind)) {
Condition.Kind.event_match -> {
if (this.key != null && this.pattern != null) {
return when (Condition.Kind.fromString(kind)) {
Condition.Kind.EventMatch -> {
if (key != null && pattern != null) {
EventMatchCondition(key, pattern)
} else {
Timber.e("Malformed Event match condition")
null
}
}
Condition.Kind.contains_display_name -> {
Condition.Kind.ContainsDisplayName -> {
ContainsDisplayNameCondition()
}
Condition.Kind.room_member_count -> {
if (this.iz.isNullOrBlank()) {
Condition.Kind.RoomMemberCount -> {
if (iz.isNullOrEmpty()) {
Timber.e("Malformed ROOM_MEMBER_COUNT condition")
null
} else {
RoomMemberCountCondition(this.iz)
RoomMemberCountCondition(iz)
}
}
Condition.Kind.sender_notification_permission -> {
this.key?.let { SenderNotificationPermissionCondition(it) }
Condition.Kind.SenderNotificationPermission -> {
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")
null
}

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Ruleset(
internal data class Ruleset(
val content: List<PushRule>? = null,
val override: 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>>()
impactedRoomsId.forEach { roomId ->
RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId).findAll()?.let { results ->
map[roomId] = results.map { it.userId }
}
RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId)
.findAll()
.let { results ->
map[roomId] = results.map { it.userId }
}
}
map.forEach { entry ->

View File

@ -38,7 +38,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
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,
ruleId = pushrule.ruleId,
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,
ruleId = pushrule.ruleId,
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
@SessionScope
internal class DefaultPushRuleService @Inject constructor(private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask,
private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
internal class DefaultPushRuleService @Inject constructor(
private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask,
private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
) : PushRuleService {
private var listeners = mutableSetOf<PushRuleService.PushRuleListener>()

View File

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

View File

@ -15,37 +15,52 @@
*/
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.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 timber.log.Timber
import im.vector.matrix.android.internal.session.room.RoomGetter
import javax.inject.Inject
// TODO Inject constructor
internal class DefaultConditionResolver(private val event: Event,
private val roomService: RoomService,
@UserId private val userId: String) : ConditionResolver {
internal class DefaultConditionResolver @Inject constructor(
private val roomGetter: RoomGetter,
@UserId private val userId: String
) : ConditionResolver {
override fun resolveEventMatchCondition(eventMatchCondition: EventMatchCondition): Boolean {
return eventMatchCondition.isSatisfied(event)
override fun resolveEventMatchCondition(event: Event,
condition: EventMatchCondition): Boolean {
return condition.isSatisfied(event)
}
override fun resolveRoomMemberCountCondition(roomMemberCountCondition: RoomMemberCountCondition): Boolean {
return roomMemberCountCondition.isSatisfied(event, roomService)
override fun resolveRoomMemberCountCondition(event: Event,
condition: RoomMemberCountCondition): Boolean {
return condition.isSatisfied(event, roomGetter)
}
override fun resolveSenderNotificationPermissionCondition(senderNotificationPermissionCondition: SenderNotificationPermissionCondition): Boolean {
// val roomId = event.roomId ?: return false
// 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 {
override fun resolveSenderNotificationPermissionCondition(event: Event,
condition: SenderNotificationPermissionCondition): Boolean {
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
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.RoomService
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.VersioningState
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.Optional
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.RoomSummaryEntityFields
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.session.room.alias.GetRoomIdByAliasTask
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.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
@ -48,15 +45,17 @@ import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService {
internal class DefaultRoomService @Inject constructor(
private val monarchy: Monarchy,
private val roomSummaryMapper: RoomSummaryMapper,
private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomGetter: RoomGetter,
private val taskExecutor: TaskExecutor
) : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
return createRoomTask
@ -67,33 +66,11 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
}
override fun getRoom(roomId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use {
if (RoomEntity.where(it, roomId).findFirst() != null) {
roomFactory.create(roomId)
} else {
null
}
}
return roomGetter.getRoom(roomId)
}
override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
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
}
return roomGetter.getDirectRoomWith(otherUserId)
}
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.MarkAllRoomsReadTask
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.ReportContentTask
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.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.SendTypingTask
import retrofit2.Retrofit
@ -72,6 +80,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindRoomFactory(factory: DefaultRoomFactory): RoomFactory
@Binds
abstract fun bindRoomGetter(getter: DefaultRoomGetter): RoomGetter
@Binds
abstract fun bindRoomService(service: DefaultRoomService): RoomService

View File

@ -54,7 +54,7 @@ internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule?
}
else -> {
val condition = PushCondition(
kind = Condition.Kind.event_match.value,
kind = Condition.Kind.EventMatch.value,
key = "room_id",
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.toContent
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.RoomMemberContent
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.mockk
import org.amshove.kluent.shouldBe
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class PushrulesConditionTest {
/* ==========================================================================================
* Test EventMatchCondition
* ========================================================================================== */
@Test
fun test_eventmatch_type_condition() {
val condition = EventMatchCondition("type", "m.room.message")
@ -120,6 +125,24 @@ class PushrulesConditionTest {
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
fun test_roommember_condition() {
val conditionEqual3 = RoomMemberCountCondition("3")
@ -137,7 +160,7 @@ class PushrulesConditionTest {
every { getNumberOfJoinedMembers() } returns 3
}
val sessionStub = mockk<RoomService> {
val roomGetterStub = mockk<RoomGetter> {
every { getRoom(room2JoinedId) } returns roomStub2Joined
every { getRoom(room3JoinedId) } returns roomStub3Joined
}
@ -148,9 +171,9 @@ class PushrulesConditionTest {
content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0,
roomId = room2JoinedId).also {
assertFalse("This room does not have 3 members", conditionEqual3.isSatisfied(it, sessionStub))
assertFalse("This room does not have 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub))
assertTrue("This room has less than 3 members", conditionLessThan3.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, roomGetterStub))
assertTrue("This room has less than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub))
}
Event(
@ -159,23 +182,36 @@ class PushrulesConditionTest {
content = MessageTextContent("m.text", "A").toContent(),
originServerTs = 0,
roomId = room3JoinedId).also {
assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, sessionStub))
assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, sessionStub))
assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, sessionStub))
assertTrue("This room has 3 members", conditionEqual3.isSatisfied(it, roomGetterStub))
assertTrue("This room has 3 members", conditionEqual3Bis.isSatisfied(it, roomGetterStub))
assertFalse("This room has more than 3 members", conditionLessThan3.isSatisfied(it, roomGetterStub))
}
}
/* ==========================================================================================
* Test ContainsDisplayNameCondition
* ========================================================================================== */
@Test
fun test_notice_condition() {
val conditionEqual = EventMatchCondition("content.msgtype", "m.notice")
fun test_displayName_condition() {
val condition = ContainsDisplayNameCondition()
Event(
val event = Event(
type = "m.room.message",
eventId = "mx0",
content = MessageTextContent("m.notice", "A").toContent(),
content = MessageTextContent("m.text", "How was the cake benoit?").toContent(),
originServerTs = 0,
roomId = "2joined").also {
assertTrue("Notice", conditionEqual.isSatisfied(it))
}
roomId = "2joined")
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.versionMinor = 15
ext.versionMinor = 16
ext.versionPatch = 0
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.ScreenComponent
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
/**
@ -87,10 +90,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
return view
}
@CallSuper
override fun onDestroyView() {
super.onDestroyView()
unBinder?.unbind()
unBinder = null
uiDisposables.clear()
}
@CallSuper
override fun onDestroy() {
uiDisposables.dispose()
super.onDestroy()
}
override fun onAttach(context: Context) {
@ -146,4 +157,29 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
protected fun setArguments(args: Parcelable? = null) {
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 im.vector.matrix.android.api.session.user.model.User
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.utils.DimensionConverter
import kotlinx.android.synthetic.main.fragment_create_direct_room.*
@ -57,8 +61,10 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
setupFilterView()
setupAddByMatrixIdView()
setupCloseView()
viewModel.selectUserEvent.observeEvent(this) {
updateChipsView(it)
viewModel.observeViewEvents {
when (it) {
is CreateDirectRoomViewEvents.SelectUserAction -> updateChipsView(it)
}.exhaustive
}
viewModel.selectSubscribe(this, CreateDirectRoomViewState::selectedUsers) {
renderSelectedUsers(it)
@ -132,7 +138,7 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
knownUsersController.setData(it)
}
private fun updateChipsView(data: SelectUserAction) {
private fun updateChipsView(data: CreateDirectRoomViewEvents.SelectUserAction) {
if (data.isAdded) {
addChipToGroup(data.user, chipGroup)
} 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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
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.util.toMatrixItem
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.utils.LiveEvent
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
@ -43,16 +38,10 @@ import java.util.concurrent.TimeUnit
private typealias KnowUsersFilter = String
private typealias DirectoryUsersSearch = String
data class SelectUserAction(
val user: User,
val isAdded: Boolean,
val index: Int
)
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState,
private val session: Session)
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, EmptyViewEvents>(initialState) {
: VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -62,10 +51,6 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
private val knownUsersFilter = BehaviorRelay.createDefault<Option<KnowUsersFilter>>(Option.empty())
private val directoryUsersSearch = BehaviorRelay.create<DirectoryUsersSearch>()
private val _selectUserEvent = MutableLiveData<LiveEvent<SelectUserAction>>()
val selectUserEvent: LiveData<LiveEvent<SelectUserAction>>
get() = _selectUserEvent
companion object : MvRxViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@JvmStatic
@ -109,7 +94,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
val index = state.selectedUsers.indexOfFirst { it.userId == action.user.userId }
val selectedUsers = state.selectedUsers.minus(action.user)
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 ->
@ -129,7 +114,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
isAddOperation = false
}
setState { copy(selectedUsers = selectedUsers) }
_selectUserEvent.postLiveEvent(SelectUserAction(action.user, isAddOperation, changeIndex))
_viewEvents.post(CreateDirectRoomViewEvents.SelectUserAction(action.user, isAddOperation, changeIndex))
}
private fun observeDirectoryUsers() {

View File

@ -23,18 +23,17 @@ import android.widget.TextView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import butterknife.BindView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
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.features.crypto.verification.choose.VerificationChooseMethodFragment
import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment
@ -56,7 +55,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
val verificationId: String? = null,
val roomId: String? = null,
// Special mode where UX should show loading wheel until other user sends a request/tx
val waitForIncomingRequest : Boolean = false
val waitForIncomingRequest: Boolean = false
) : Parcelable
@Inject
@ -84,17 +83,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.requestLiveData.observe(viewLifecycleOwner, Observer {
it.peekContent().let { va ->
when (va) {
is Success -> {
if (va.invoke() is VerificationAction.GotItConclusion) {
dismiss()
}
}
}
}
})
viewModel.observeViewEvents {
when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
}.exhaustive
}
}
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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
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.internal.crypto.verification.PendingVerificationRequest
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.utils.LiveEvent
data class VerificationBottomSheetViewState(
val otherUserMxItem: MatrixItem? = null,
@ -63,14 +59,9 @@ data class VerificationBottomSheetViewState(
class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: VerificationBottomSheetViewState,
@Assisted args: VerificationBottomSheet.VerificationArgs,
private val session: Session)
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction, EmptyViewEvents>(initialState),
: VectorViewModel<VerificationBottomSheetViewState, VerificationAction, VerificationBottomSheetViewEvents>(initialState),
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 {
session.getVerificationService().addListener(this)
@ -255,7 +246,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
?.shortCodeDoesNotMatch()
}
is VerificationAction.GotItConclusion -> {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
}.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.core.extensions.cleanup
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.VectorBaseFragment
import im.vector.riotx.features.home.HomeActivitySharedAction
@ -51,8 +51,10 @@ class GroupListFragment @Inject constructor(
stateView.contentView = groupListView
groupListView.configureWith(groupController)
viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) {
sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
viewModel.observeViewEvents {
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");
* 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
* 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,
@ -14,12 +14,13 @@
* 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,
val file: File?,
val throwable: Throwable?
)
/**
* Transient events for group list screen
*/
sealed class GroupListViewEvents : VectorViewEvents {
object OpenGroupSummary : GroupListViewEvents()
}

View File

@ -17,8 +17,6 @@
package im.vector.riotx.features.grouplist
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext
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.rx.rx
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.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
@ -46,7 +41,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private val selectedGroupStore: SelectedGroupDataSource,
private val session: Session,
private val stringProvider: StringProvider
) : VectorViewModel<GroupListViewState, GroupListAction, EmptyViewEvents>(initialState) {
) : VectorViewModel<GroupListViewState, GroupListAction, GroupListViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -62,9 +57,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
}
}
private val _openGroupLiveData = MutableLiveData<LiveEvent<GroupSummary>>()
val openGroupLiveData: LiveData<LiveEvent<GroupSummary>>
get() = _openGroupLiveData
private var currentGroupId = ""
init {
observeGroupSummaries()
@ -74,10 +67,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
private fun observeSelectionState() {
selectSubscribe(GroupListViewState::selectedGroup) { groupSummary ->
if (groupSummary != null) {
val selectedGroup = _openGroupLiveData.value?.peekContent()
// We only want to open group if the updated selectedGroup is a different one.
if (selectedGroup?.groupId != groupSummary.groupId) {
_openGroupLiveData.postLiveEvent(groupSummary)
if (currentGroupId != groupSummary.groupId) {
currentGroupId = groupSummary.groupId
_viewEvents.post(GroupListViewEvents.OpenGroupSummary)
}
val optionGroup = Option.just(groupSummary)
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.exhaustive
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.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager
@ -254,12 +253,7 @@ class RoomDetailFragment @Inject constructor(
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
}
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
.observe()
.subscribe {
@ -267,34 +261,10 @@ class RoomDetailFragment @Inject constructor(
}
.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")) {
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 ->
when (mode) {
is SendMode.REGULAR -> renderRegularMode(mode.text)
@ -308,14 +278,17 @@ class RoomDetailFragment @Inject constructor(
syncStateView.render(syncState)
}
roomDetailViewModel.requestLiveData.observeEvent(this) {
displayRoomDetailActionResult(it)
}
roomDetailViewModel.observeViewEvents {
when (it) {
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
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
}
}
@ -370,18 +343,38 @@ class RoomDetailFragment @Inject constructor(
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())
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.error_file_too_big,
error.filename,
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
action.filename,
TextUtils.formatFileSize(requireContext(), action.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), action.homeServerLimitInBytes)
))
.setPositiveButton(R.string.ok, null)
.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() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate {
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) {
is SendMessageResult.MessageSent -> {
is RoomDetailViewEvents.MessageSent -> {
updateComposerText("")
}
is SendMessageResult.SlashCommandHandled -> {
is RoomDetailViewEvents.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
updateComposerText("")
}
is SendMessageResult.SlashCommandError -> {
is RoomDetailViewEvents.SlashCommandError -> {
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))
}
is SendMessageResult.SlashCommandResultOk -> {
is RoomDetailViewEvents.SlashCommandResultOk -> {
updateComposerText("")
}
is SendMessageResult.SlashCommandResultError -> {
is RoomDetailViewEvents.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage)
}
is SendMessageResult.SlashCommandNotImplemented -> {
is RoomDetailViewEvents.SlashCommandNotImplemented -> {
displayCommandError(getString(R.string.not_implemented))
}
}
} // .exhaustive
lockSendButton = false
}
@ -814,84 +807,81 @@ class RoomDetailFragment @Inject constructor(
.show()
}
private fun displayRoomDetailActionResult(result: Async<RoomDetailAction>) {
when (result) {
is Fail -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.error))
.setPositiveButton(R.string.ok, null)
.show()
}
is Success -> {
when (val data = result.invoke()) {
is RoomDetailAction.ReportContent -> {
when {
data.spam -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(data.senderId))
}
.show()
.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)
}
}
private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
private fun displayRoomDetailActionSuccess(result: RoomDetailViewEvents.ActionSuccess) {
when (val data = result.action) {
is RoomDetailAction.ReportContent -> {
when {
data.spam -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_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 -> {
Timber.v("## SAS RequestVerification action")
VerificationBottomSheet.withArgs(
roomDetailArgs.roomId,
data.userId
).show(parentFragmentManager, "REQ")
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)
}
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")
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 -> {
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 {
permalinkHandler

View File

@ -16,7 +16,10 @@
package im.vector.riotx.features.home.room.detail
import androidx.annotation.StringRes
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.features.command.Command
import java.io.File
/**
* Transient events for RoomDetail
@ -24,4 +27,34 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class RoomDetailViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : 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 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.MvRxViewModelFactory
import com.airbnb.mvrx.Success
@ -32,6 +28,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
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.session.Session
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.riotx.R
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.resources.StringProvider
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.features.command.CommandParser
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.home.room.detail.composer.rainbow.RainbowGenerator
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.settings.VectorPreferences
@ -116,11 +110,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
var timeline = room.createTimeline(eventId, timelineSettings)
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
var pendingAction: RoomDetailAction? = null
// 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) {
R.id.clear_message_queue ->
/* 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 -> {
// Send the text message to the room
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))
_viewEvents.post(RoomDetailViewEvents.SlashCommandError(slashCommandResult.command))
}
is ParsedCommand.ErrorEmptySlashCommand -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown("/"))
_viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown("/"))
}
is ParsedCommand.ErrorUnknownSlashCommand -> {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))
_viewEvents.post(RoomDetailViewEvents.SlashCommandUnknown(slashCommandResult.slashCommand))
}
is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult)
@ -378,55 +346,55 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
is ParsedCommand.SetUserPowerLevel -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.ClearScalarToken -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.SetMarkdown -> {
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))
popDraft()
}
is ParsedCommand.UnbanUser -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.BanUser -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.KickUser -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.JoinRoom -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.PartRoom -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbow -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendRainbowEmote -> {
slashCommandResult.message.toString().let {
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendSpoiler -> {
@ -434,7 +402,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
"<span data-mx-spoiler>${slashCommandResult.message}</span>"
)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendShrug -> {
@ -446,12 +414,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
room.sendTextMessage(sequence)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.VerifyUser -> {
session.getVerificationService().requestKeyVerificationInDMs(supportedVerificationMethods, slashCommandResult.userId, room.roomId)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.ChangeTopic -> {
@ -460,7 +428,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
is ParsedCommand.ChangeDisplayName -> {
// TODO
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
}
}.exhaustive
}
@ -487,7 +455,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
Timber.w("Same message content, do not send edition")
}
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
@ -510,13 +478,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else {
room.sendFormattedTextMessage(finalText, htmlText)
}
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
}
@ -549,29 +517,29 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
room.updateTopic(changeTopic.topic, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
}
override fun onFailure(failure: Throwable) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultError(failure))
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
})
}
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
room.invite(invite.userId, invite.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
}
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 {
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError(
tooBigFile.name ?: tooBigFile.path,
tooBigFile.size,
maxUploadFileSize
))
}
}
}
@ -721,7 +692,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
action.messageFileContent.encryptedFileInfo?.toElementToDecrypt(),
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
_downloadedFileEvent.postLiveEvent(DownloadFileState(
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.getMimeType(),
data,
null
@ -729,7 +700,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
override fun onFailure(failure: Throwable) {
_downloadedFileEvent.postLiveEvent(DownloadFileState(
_viewEvents.post(RoomDetailViewEvents.DownloadFileState(
action.messageFileContent.getMimeType(),
null,
failure
@ -750,7 +721,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
if (action.highlight) {
setState { copy(highlightedEventId = correctedEventId) }
}
_navigateToEvent.postLiveEvent(correctedEventId)
_viewEvents.post(RoomDetailViewEvents.NavigateToEvent(correctedEventId))
}
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
@ -821,11 +792,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleReportContent(action: RoomDetailAction.ReportContent) {
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
}
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> {
override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
}
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,
room.roomId,
action.transactionId)) {
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
} else {
// TODO
}
@ -869,7 +840,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleRequestVerification(action: RoomDetailAction.RequestVerification) {
if (action.userId == session.myUserId) return
_requestLiveData.postValue(LiveEvent(Success(action)))
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action))
}
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 {
if (it.handledByOtherSession) return
if (!it.isFinished) {
_requestLiveData.postValue(LiveEvent(Success(action.copy(
_viewEvents.post(RoomDetailViewEvents.ActionSuccess(action.copy(
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.core.extensions.cleanup
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 io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.*
@ -75,13 +75,20 @@ class PublicRoomsFragment @Inject constructor(
sharedActionViewModel.post(RoomDirectorySharedAction.CreateRoom)
}
// TODO remove this, replace by ViewEvents
viewModel.joinRoomErrorLiveData.observeEvent(this) { throwable ->
Snackbar.make(publicRoomsCoordinator, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
.show()
viewModel.observeViewEvents {
handleViewEvents(it)
}
}
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() {
publicRoomsController.callback = null
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");
* 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
* 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,
@ -14,10 +14,13 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
package im.vector.riotx.features.roomdirectory
data class FileTooBigError(
val filename: String,
val fileSizeInBytes: Long,
val homeServerLimitInBytes: Long
)
import im.vector.riotx.core.platform.VectorViewEvents
/**
* 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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail
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.AssistedInject
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.util.Cancelable
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.utils.LiveEvent
import timber.log.Timber
private const val PUBLIC_ROOMS_LIMIT = 20
class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: PublicRoomsViewState,
private val session: Session)
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, EmptyViewEvents>(initialState) {
: VectorViewModel<PublicRoomsViewState, RoomDirectoryAction, RoomDirectoryViewEvents>(initialState) {
@AssistedInject.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 currentTask: Cancelable? = null
@ -109,9 +106,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomDirectoryAction) {
when (action) {
is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
is RoomDirectoryAction.FilterWith -> filterWith(action)
RoomDirectoryAction.LoadMore -> loadMore()
is RoomDirectoryAction.JoinRoom -> joinRoom(action)
is RoomDirectoryAction.FilterWith -> filterWith(action)
RoomDirectoryAction.LoadMore -> loadMore()
is RoomDirectoryAction.JoinRoom -> joinRoom(action)
}
}
@ -227,7 +224,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun onFailure(failure: Throwable) {
// Notify the user
_joinRoomErrorLiveData.postLiveEvent(failure)
_viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
setState {
copy(

View File

@ -20,8 +20,7 @@ package im.vector.riotx.features.roommemberprofile
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomMemberProfileAction : VectorViewModelAction {
object RetryFetchingInfo: RoomMemberProfileAction()
object IgnoreUser: RoomMemberProfileAction()
data class VerifyUser(val userId: String? = null, val roomId: String? = null, val canCrossSign: Boolean? = true): RoomMemberProfileAction()
object RetryFetchingInfo : RoomMemberProfileAction()
object IgnoreUser : RoomMemberProfileAction()
object VerifyUser : 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.configureWith
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.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
@ -94,33 +93,27 @@ class RoomMemberProfileFragment @Inject constructor(
is RoomMemberProfileViewEvents.Loading -> showLoading(it.message)
is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it)
}.exhaustive
}
viewModel.actionResultLiveData.observeEvent(this) { async ->
when (async) {
is Success -> {
when (val action = async.invoke()) {
is RoomMemberProfileAction.VerifyUser -> {
if (action.canCrossSign == true) {
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = action.userId!!)
.show(parentFragmentManager, "VERIF")
} else {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.verify_cannot_cross_sign)
.setPositiveButton(R.string.verification_profile_verify) { _, _ ->
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = action.userId!!)
.show(parentFragmentManager, "VERIF")
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
private fun handleStartVerification(startVerification: RoomMemberProfileViewEvents.StartVerification) {
if (startVerification.canCrossSign) {
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = startVerification.userId)
.show(parentFragmentManager, "VERIF")
} else {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.verify_cannot_cross_sign)
.setPositiveButton(R.string.verification_profile_verify) { _, _ ->
VerificationBottomSheet
.withArgs(roomId = null, otherUserId = startVerification.userId)
.show(parentFragmentManager, "VERIF")
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
@ -197,7 +190,7 @@ class RoomMemberProfileFragment @Inject constructor(
}
override fun onTapVerify() {
viewModel.handle(RoomMemberProfileAction.VerifyUser())
viewModel.handle(RoomMemberProfileAction.VerifyUser)
}
override fun onShowDeviceList() = withState(viewModel) {

View File

@ -26,4 +26,9 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : 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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
@ -49,7 +46,6 @@ import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
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) {
session.getRoom(initialState.roomId)
} else {
@ -145,23 +137,19 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
when (action) {
is RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo()
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
if (state.isRoomEncrypted) {
if (!state.isMine && state.userMXCrossSigningInfo?.isTrusted() == false) {
// ok, let's find or create the DM room
_actionResultLiveData.postValue(
LiveEvent(Success(
action.copy(
userId = state.userId,
canCrossSign = session.getCrossSigningService().canCrossSign()
)
))
)
_viewEvents.post(RoomMemberProfileViewEvents.StartVerification(
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 androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
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.features.crypto.verification.VerificationAction
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import javax.inject.Inject
import kotlin.reflect.KClass
@ -48,20 +46,16 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.requestLiveData.observeEvent(this) { async ->
when (async) {
is Success -> {
when (val action = async.invoke()) {
is VerificationAction.StartSASVerification -> {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = action.otherUserId,
transactionId = action.pendingRequestTransactionId
).show(requireActivity().supportFragmentManager, "REQPOP")
}
}
viewModel.observeViewEvents {
when (it) {
is DeviceListBottomSheetViewEvents.Verify -> {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = it.userId,
transactionId = it.txID
).show(requireActivity().supportFragmentManager, "REQPOP")
}
}
}.exhaustive
}
}
@ -69,7 +63,7 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() {
withState(viewModel) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (it.selectedDevice != null) {
viewModel.selectDevice(null)
viewModel.handle(DeviceListAction.DeselectDevice)
return@withState true
} else {
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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
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.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyAction
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.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.VerificationAction
data class DeviceListViewState(
val userItem: MatrixItem? = null,
@ -52,14 +45,8 @@ data class DeviceListViewState(
class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState,
@Assisted private val userId: String,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<DeviceListViewState, EmptyAction, EmptyViewEvents>(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
: VectorViewModel<DeviceListViewState, DeviceListAction, DeviceListBottomSheetViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
@ -67,7 +54,6 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva
}
init {
session.rx().liveUserCryptoDevices(userId)
.execute {
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 ->
if (state.selectedDevice != null) {
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 {
copy(selectedDevice = device)
copy(selectedDevice = action.device)
}
}
fun manuallyVerify(device: CryptoDeviceInfo) {
session.getVerificationService().beginKeyVerification(VerificationMethod.SAS, userId, device.deviceId, null)?.let { txID ->
_requestLiveData.postValue(LiveEvent(Success(VerificationAction.StartSASVerification(userId, txID))))
private fun deselectDevice() {
setState {
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> {
@JvmStatic

View File

@ -62,6 +62,6 @@ class DeviceListFragment @Inject constructor(
}
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) {
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.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup
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.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
@ -44,23 +41,20 @@ class CrossSigningSettingsFragment @Inject constructor(
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.requestLiveData.observeEvent(this) {
viewModel.observeViewEvents {
when (it) {
is Fail -> {
is CrossSigningSettingsViewEvents.Failure -> {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_error)
.setMessage(it.error.message)
.setMessage(errorFormatter.toHumanReadable(it.throwable))
.setPositiveButton(R.string.ok, null)
.show()
Unit
}
is Success -> {
when (val action = it.invoke()) {
is CrossSigningAction.RequestPasswordAuth -> {
requestPassword(action.sessionId)
}
}
is CrossSigningSettingsViewEvents.RequestPassword -> {
requestPassword()
}
}
}.exhaustive
}
}
@ -89,18 +83,14 @@ class CrossSigningSettingsFragment @Inject constructor(
super.onDestroyView()
}
private fun requestPassword(sessionId: String) {
private fun requestPassword() {
PromptPasswordDialog().show(requireActivity()) { password ->
// TODO sessionId should never get out the ViewModel
viewModel.handle(CrossSigningAction.InitializeCrossSigning(UserPasswordAuth(
session = sessionId,
password = password
)))
viewModel.handle(CrossSigningAction.PasswordEntered(password))
}
}
override fun onInitializeCrossSigningKeys() {
viewModel.handle(CrossSigningAction.InitializeCrossSigning())
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
}
override fun onResetCrossSigningKeys() {
@ -108,7 +98,7 @@ class CrossSigningSettingsFragment @Inject constructor(
.setTitle(R.string.dialog_title_confirmation)
.setMessage(R.string.are_you_sure)
.setPositiveButton(R.string.ok) { _, _ ->
viewModel.handle(CrossSigningAction.InitializeCrossSigning())
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
}
.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
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.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
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.di.MoshiProvider
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.VectorViewModelAction
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.LiveEvent
data class CrossSigningSettingsViewState(
val crossSigningInfo: MXCrossSigningInfo? = null,
@ -51,19 +44,13 @@ data class CrossSigningSettingsViewState(
) : MvRxState
sealed class CrossSigningAction : VectorViewModelAction {
data class InitializeCrossSigning(val auth: UserPasswordAuth? = null) : CrossSigningAction()
data class RequestPasswordAuth(val sessionId: String) : CrossSigningAction()
object InitializeCrossSigning : CrossSigningAction()
data class PasswordEntered(val password: String) : CrossSigningAction()
}
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, EmptyViewEvents>(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
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, CrossSigningSettingsViewEvents>(initialState) {
init {
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
interface Factory {
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
@ -89,26 +79,37 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
override fun handle(action: CrossSigningAction) {
when (action) {
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?) {
_pendingSession = null
setState {
copy(isUploadingKeys = true)
}
session.getCrossSigningService().initializeCrossSigning(auth, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_pendingSession = null
setState {
copy(isUploadingKeys = false)
}
}
override fun onFailure(failure: Throwable) {
if (failure is Failure.OtherServerError
&& failure.httpCode == 401
) {
_pendingSession = null
if (failure is Failure.OtherServerError && failure.httpCode == 401) {
try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
@ -118,23 +119,23 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
}?.let { flowResponse ->
// Retry with authentication
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
} else {
_requestLiveData.postValue(LiveEvent(Fail(Throwable("You cannot do that from mobile"))))
// can't do this from here
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
setState {
copy(isUploadingKeys = false)
}
return
}
}
}
when (failure) {
is Failure.ServerError -> {
_requestLiveData.postValue(LiveEvent(Fail(Throwable(failure.error.message))))
}
else -> {
_requestLiveData.postValue(LiveEvent(Fail(failure)))
}
}
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
setState {
copy(isUploadingKeys = false)
}

View File

@ -16,14 +16,14 @@
package im.vector.riotx.features.settings.devices
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction()
data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, 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 Rename(val deviceId: String, val newName: String) : 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
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.core.platform.VectorViewEvents
/**
@ -25,4 +26,13 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class DevicesViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : 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
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
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.DevicesListResponse
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.utils.LiveEvent
import im.vector.riotx.features.crypto.verification.supportedVerificationMethods
data class DevicesViewState(
@ -76,15 +72,6 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
private var _currentDeviceId: 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 {
refreshDevicesList()
session.getVerificationService().addListener(this)
@ -187,25 +174,21 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
val txID = session.getVerificationService().requestKeyVerification(supportedVerificationMethods, session.myUserId, listOf(action.deviceId))
_requestLiveData.postValue(LiveEvent(Success(
action.copy(
userId = session.myUserId,
transactionId = txID.transactionId
)
)))
_viewEvents.post(DevicesViewEvents.ShowVerifyDevice(
session.myUserId,
txID.transactionId
))
}
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
if (info == null) {
_requestLiveData.postValue(LiveEvent(Uninitialized))
} else {
_requestLiveData.postValue(LiveEvent(Success(action.copy(deviceInfo = info))))
if (info != null) {
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info))
}
}
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) {
setState {
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 com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
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.configureWith
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.VectorBaseFragment
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
@ -64,35 +62,18 @@ class VectorSettingsDevicesFragment @Inject constructor(
recyclerView.configureWith(devicesController, showDivider = true)
viewModel.observeViewEvents {
when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive
}
viewModel.requestPasswordLiveData.observeEvent(this) {
maybeShowDeleteDeviceWithPasswordDialog()
}
viewModel.fragmentActionLiveData.observeEvent(this) { async ->
when (async) {
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")
}
}
}
is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable)
is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog()
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
is DevicesViewEvents.ShowVerifyDevice -> {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = it.userId,
transactionId = it.transactionId
).show(childFragmentManager, "REQPOP")
}
}
}.exhaustive
}
}
@ -152,7 +133,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
.setPositiveButton(R.string.ok) { _, _ ->
val newName = input.text.toString()
viewModel.handle(DevicesAction.Rename(deviceInfo, newName))
viewModel.handle(DevicesAction.Rename(deviceInfo.deviceId!!, newName))
}
.setNegativeButton(R.string.cancel, null)
.show()