Improve a bit how joining/leaving are handled

This commit is contained in:
ganfra 2020-07-10 20:08:51 +02:00
parent 8814364497
commit 150d44aafd
39 changed files with 454 additions and 284 deletions

View file

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.identity.ThreePid import im.vector.matrix.android.api.session.identity.ThreePid
import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
@ -165,6 +166,10 @@ class RxSession(private val session: Session) {
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
} }
} }
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
return session.getChangeMembershipsLive().asObservable()
}
} }
fun Session.rx(): RxSession { fun Session.rx(): RxSession {

View file

@ -34,13 +34,6 @@ interface RoomDirectoryService {
publicRoomsParams: PublicRoomsParams, publicRoomsParams: PublicRoomsParams,
callback: MatrixCallback<PublicRoomsResponse>): Cancelable callback: MatrixCallback<PublicRoomsResponse>): Cancelable
/**
* Join a room by id, or room alias
*/
fun joinRoom(roomIdOrAlias: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Fetches the overall metadata about protocols supported by the homeserver. * Fetches the overall metadata about protocols supported by the homeserver.
* Includes both the available protocols and all fields required for queries against each protocol. * Includes both the available protocols and all fields required for queries against each protocol.

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
@ -104,5 +105,13 @@ interface RoomService {
searchOnServer: Boolean, searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable callback: MatrixCallback<Optional<String>>): Cancelable
/**
* Return a live data of all local changes membership who happened since the session has been opened.
* It allows you to track this in your client to known what is currently being processed by the SDK.
* It won't know anything about change being done in other client.
* Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
*/
fun getChangeMembershipsLive(): LiveData<Map<String,ChangeMembershipState>>
fun getExistingDirectRoomWithUser(otherUserId: String) : Room? fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
} }

View file

@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService] * [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
*/ */
data class RoomSummaryQueryParams( data class RoomSummaryQueryParams(
val roomId: QueryStringValue,
val displayName: QueryStringValue, val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue, val canonicalAlias: QueryStringValue,
val memberships: List<Membership> val memberships: List<Membership>
@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
class Builder { class Builder {
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = Membership.all() var memberships: List<Membership> = Membership.all()
fun build() = RoomSummaryQueryParams( fun build() = RoomSummaryQueryParams(
roomId = roomId,
displayName = displayName, displayName = displayName,
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
memberships = memberships memberships = memberships

View file

@ -0,0 +1,34 @@
/*
* 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.api.session.room.members
sealed class ChangeMembershipState() {
object Unknown : ChangeMembershipState()
object Joining : ChangeMembershipState()
data class FailedJoining(val throwable: Throwable) : ChangeMembershipState()
object Joined : ChangeMembershipState()
object Leaving : ChangeMembershipState()
data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState()
object Left : ChangeMembershipState()
fun isInProgress() = this is Joining || this is Leaving
fun isSuccessful() = this is Joined || this is Left
fun isFailed() = this is FailedJoining || this is FailedLeaving
}

View file

@ -44,14 +44,6 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun joinRoom(roomIdOrAlias: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask
.configureWith(JoinRoomTask.Params(roomIdOrAlias, reason)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable { override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable {
return getThirdPartyProtocolsTask return getThirdPartyProtocolsTask
.configureWith { .configureWith {

View file

@ -21,12 +21,14 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
@ -43,6 +45,7 @@ internal class DefaultRoomService @Inject constructor(
private val roomIdByAliasTask: GetRoomIdByAliasTask, private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomGetter: RoomGetter, private val roomGetter: RoomGetter,
private val roomSummaryDataSource: RoomSummaryDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : RoomService { ) : RoomService {
@ -111,4 +114,8 @@ internal class DefaultRoomService @Inject constructor(
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>> {
return roomChangeMembershipStateDataSource.getLiveStates()
}
} }

View file

@ -0,0 +1,67 @@
/*
* 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.membership
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.session.SessionScope
import javax.inject.Inject
/**
* This class holds information about rooms that current user is joining or leaving.
*/
@SessionScope
internal class RoomChangeMembershipStateDataSource @Inject constructor() {
private val mutableLiveStates = MutableLiveData<Map<String, ChangeMembershipState>>(emptyMap())
private val states = HashMap<String, ChangeMembershipState>()
/**
* This will update local states to be synced with the server.
*/
fun setMembershipFromSync(roomId: String, membership: Membership) {
if (states.containsKey(roomId)) {
val newState = membership.toMembershipChangeState()
updateState(roomId, newState)
}
}
fun updateState(roomId: String, state: ChangeMembershipState) {
states[roomId] = state
mutableLiveStates.postValue(states.toMap())
}
fun getLiveStates(): LiveData<Map<String, ChangeMembershipState>> {
return mutableLiveStates
}
fun getState(roomId: String): ChangeMembershipState {
return states.getOrElse(roomId) {
ChangeMembershipState.Unknown
}
}
private fun Membership.toMembershipChangeState(): ChangeMembershipState {
return when {
this == Membership.JOIN -> ChangeMembershipState.Joined
this.isLeft() -> ChangeMembershipState.Left
else -> ChangeMembershipState.Unknown
}
}
}

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.membership.joining package im.vector.matrix.android.internal.session.room.membership.joining
import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure import im.vector.matrix.android.api.session.room.failure.JoinRoomFailure
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse import im.vector.matrix.android.api.session.room.model.create.JoinRoomResponse
import im.vector.matrix.android.internal.database.awaitNotEmptyResult import im.vector.matrix.android.internal.database.awaitNotEmptyResult
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
@ -24,6 +25,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntityFields
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -45,12 +47,19 @@ internal class DefaultJoinRoomTask @Inject constructor(
private val readMarkersTask: SetReadMarkersTask, private val readMarkersTask: SetReadMarkersTask,
@SessionDatabase @SessionDatabase
private val realmConfiguration: RealmConfiguration, private val realmConfiguration: RealmConfiguration,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val eventBus: EventBus private val eventBus: EventBus
) : JoinRoomTask { ) : JoinRoomTask {
override suspend fun execute(params: JoinRoomTask.Params) { override suspend fun execute(params: JoinRoomTask.Params) {
val joinRoomResponse = executeRequest<JoinRoomResponse>(eventBus) { roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason)) val joinRoomResponse = try {
executeRequest<JoinRoomResponse>(eventBus) {
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason))
}
} catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))
throw failure
} }
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
val roomId = joinRoomResponse.roomId val roomId = joinRoomResponse.roomId

View file

@ -19,12 +19,16 @@ package im.vector.matrix.android.internal.session.room.membership.leaving
import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.state.StateEventDataSource import im.vector.matrix.android.internal.session.room.state.StateEventDataSource
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryDataSource
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> { internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
@ -37,7 +41,9 @@ internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
internal class DefaultLeaveRoomTask @Inject constructor( internal class DefaultLeaveRoomTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val eventBus: EventBus, private val eventBus: EventBus,
private val stateEventDataSource: StateEventDataSource private val stateEventDataSource: StateEventDataSource,
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
) : LeaveRoomTask { ) : LeaveRoomTask {
override suspend fun execute(params: LeaveRoomTask.Params) { override suspend fun execute(params: LeaveRoomTask.Params) {
@ -45,13 +51,29 @@ internal class DefaultLeaveRoomTask @Inject constructor(
} }
private suspend fun leaveRoom(roomId: String, reason: String?) { private suspend fun leaveRoom(roomId: String, reason: String?) {
val roomCreateStateEvent = stateEventDataSource.getStateEvent(roomId, eventType = EventType.STATE_ROOM_CREATE, stateKey = QueryStringValue.NoCondition) val roomSummary = roomSummaryDataSource.getRoomSummary(roomId)
val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel<RoomCreateContent>()?.predecessor?.roomId if (roomSummary?.membership?.isActive() == false) {
executeRequest<Unit>(eventBus) { Timber.v("Room $roomId is not joined so can't be left")
apiCall = roomAPI.leave(roomId, mapOf("reason" to reason)) return
} }
roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.Leaving)
val roomCreateStateEvent = stateEventDataSource.getStateEvent(
roomId = roomId,
eventType = EventType.STATE_ROOM_CREATE,
stateKey = QueryStringValue.NoCondition
)
// Server is not cleaning predecessor rooms, so we also try to left them
val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel<RoomCreateContent>()?.predecessor?.roomId
if (predecessorRoomId != null) { if (predecessorRoomId != null) {
leaveRoom(predecessorRoomId, reason) leaveRoom(predecessorRoomId, reason)
} }
try {
executeRequest<Unit>(eventBus) {
apiCall = roomAPI.leave(roomId, mapOf("reason" to reason))
}
} catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(roomId, ChangeMembershipState.FailedLeaving(failure))
throw failure
}
} }
} }

View file

@ -100,6 +100,7 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> { private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
val query = RoomSummaryEntity.where(realm) val query = RoomSummaryEntity.where(realm)
query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)

View file

@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.room.membership.RoomChangeMembershipStateDataSource
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.summary.RoomSummaryUpdater
@ -73,6 +74,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val cryptoService: DefaultCryptoService, private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String, @UserId private val userId: String,
private val eventBus: EventBus, private val eventBus: EventBus,
private val timelineEventDecryptor: TimelineEventDecryptor) { private val timelineEventDecryptor: TimelineEventDecryptor) {
@ -185,6 +187,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} != null } != null
roomTypingUsersHandler.handle(realm, roomId, ephemeralResult) roomTypingUsersHandler.handle(realm, roomId, ephemeralResult)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.JOIN)
roomSummaryUpdater.update( roomSummaryUpdater.update(
realm, realm,
roomId, roomId,
@ -221,6 +224,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val inviterEvent = roomSync.inviteState?.events?.lastOrNull { val inviterEvent = roomSync.inviteState?.events?.lastOrNull {
it.type == EventType.STATE_ROOM_MEMBER it.type == EventType.STATE_ROOM_MEMBER
} }
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId) roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId)
return roomEntity return roomEntity
} }
@ -263,6 +267,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val membership = leftMember?.membership ?: Membership.LEAVE val membership = leftMember?.membership ?: Membership.LEAVE
roomEntity.membership = membership roomEntity.membership = membership
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
roomTypingUsersHandler.handle(realm, roomId, null)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications) roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications)
return roomEntity return roomEntity
} }

View file

@ -883,7 +883,7 @@ class RoomDetailFragment @Inject constructor(
} else if (summary?.membership == Membership.INVITE && inviter != null) { } else if (summary?.membership == Membership.INVITE && inviter != null) {
roomToolbarContentView.isClickable = false roomToolbarContentView.isClickable = false
inviteView.visibility = View.VISIBLE inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE) inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState)
// Intercept click event // Intercept click event
inviteView.setOnClickListener { } inviteView.setOnClickListener { }
} else if (state.asyncInviter.complete) { } else if (state.asyncInviter.complete) {

View file

@ -40,6 +40,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
@ -166,6 +167,7 @@ class RoomDetailViewModel @AssistedInject constructor(
timeline.start() timeline.start()
timeline.addListener(this) timeline.addListener(this)
observeRoomSummary() observeRoomSummary()
observeMembershipChanges()
observeSummaryState() observeSummaryState()
getUnreadState() getUnreadState()
observeSyncState() observeSyncState()
@ -406,7 +408,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->
if(state.asyncRoomSummary()?.membership != Membership.JOIN){ if (state.asyncRoomSummary()?.membership != Membership.JOIN) {
return@withState false return@withState false
} }
when (itemId) { when (itemId) {
@ -629,7 +631,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) {
session.joinRoom(command.roomAlias, command.reason, object : MatrixCallback<Unit> { session.joinRoom(command.roomAlias, command.reason, emptyList(), object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
session.getRoomSummary(command.roomAlias) session.getRoomSummary(command.roomAlias)
?.roomId ?.roomId
@ -1147,6 +1149,19 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.map {
it[initialState.roomId] ?: ChangeMembershipState.Unknown
}
.distinctUntilChanged()
.subscribe {
setState { copy(changeMembershipState = it) }
}
.disposeOnClear()
}
private fun observeSummaryState() { private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
roomSummaryHolder.set(summary) roomSummaryHolder.set(summary)

View file

@ -20,6 +20,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -64,6 +65,7 @@ data class RoomDetailViewState(
val highlightedEventId: String? = null, val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown, val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true, val canShowJumpToReadMarker: Boolean = true,
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
val canSendMessage: Boolean = true val canSendMessage: Boolean = true
) : MvRxState { ) : MvRxState {

View file

@ -19,9 +19,9 @@ package im.vector.riotx.features.home.room.list
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.ButtonStateView import im.vector.riotx.core.platform.ButtonStateView
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.invite.InviteButtonStateBinder
@EpoxyModelClass(layout = R.layout.item_room_invitation) @EpoxyModelClass(layout = R.layout.item_room_invitation)
abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>() { abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>() {
@ -37,53 +38,36 @@ abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>(
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var secondLine: CharSequence? = null @EpoxyAttribute var secondLine: CharSequence? = null
@EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var invitationAcceptInProgress: Boolean = false @EpoxyAttribute lateinit var changeMembershipState: ChangeMembershipState
@EpoxyAttribute var invitationAcceptInError: Boolean = false
@EpoxyAttribute var invitationRejectInProgress: Boolean = false
@EpoxyAttribute var invitationRejectInError: Boolean = false
@EpoxyAttribute var acceptListener: (() -> Unit)? = null @EpoxyAttribute var acceptListener: (() -> Unit)? = null
@EpoxyAttribute var rejectListener: (() -> Unit)? = null @EpoxyAttribute var rejectListener: (() -> Unit)? = null
private val acceptCallback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
acceptListener?.invoke()
}
override fun onRetryClicked() {
acceptListener?.invoke()
}
}
private val rejectCallback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
rejectListener?.invoke()
}
override fun onRetryClicked() {
rejectListener?.invoke()
}
}
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() } holder.rootView.setOnClickListener { listener?.invoke() }
holder.acceptView.callback = acceptCallback
// When a request is in progress (accept or reject), we only use the accept State button holder.rejectView.callback = rejectCallback
val requestInProgress = invitationAcceptInProgress || invitationRejectInProgress InviteButtonStateBinder.bind(holder.acceptView, holder.rejectView, changeMembershipState)
when {
requestInProgress -> holder.acceptView.render(ButtonStateView.State.Loading)
invitationAcceptInError -> holder.acceptView.render(ButtonStateView.State.Error)
else -> holder.acceptView.render(ButtonStateView.State.Button)
}
// ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
holder.acceptView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
acceptListener?.invoke()
}
override fun onRetryClicked() {
acceptListener?.invoke()
}
}
holder.rejectView.isVisible = !requestInProgress
when {
invitationRejectInError -> holder.rejectView.render(ButtonStateView.State.Error)
else -> holder.rejectView.render(ButtonStateView.State.Button)
}
holder.rejectView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
rejectListener?.invoke()
}
override fun onRetryClicked() {
rejectListener?.invoke()
}
}
holder.titleView.text = matrixItem.getBestName() holder.titleView.text = matrixItem.getBestName()
holder.subtitleView.setTextOrHide(secondLine) holder.subtitleView.setTextOrHide(secondLine)
avatarRenderer.render(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)

View file

@ -21,10 +21,12 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource import im.vector.riotx.core.utils.DataSource
@ -55,6 +57,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
init { init {
observeRoomSummaries() observeRoomSummaries()
observeMembershipChanges()
} }
override fun handle(action: RoomListAction) { override fun handle(action: RoomListAction) {
@ -102,37 +105,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
.observeOn(Schedulers.computation()) .observeOn(Schedulers.computation())
.map { buildRoomSummaries(it) } .map { buildRoomSummaries(it) }
.execute { async -> .execute { async ->
val invitedRooms = async()?.get(RoomCategory.INVITE)?.map { it.roomId }.orEmpty() copy(asyncFilteredRooms = async)
val remainingJoining = joiningRoomsIds.intersect(invitedRooms)
val remainingJoinErrors = joiningErrorRoomsIds.intersect(invitedRooms)
val remainingRejecting = rejectingRoomsIds.intersect(invitedRooms)
val remainingRejectErrors = rejectingErrorRoomsIds.intersect(invitedRooms)
copy(
asyncFilteredRooms = async,
joiningRoomsIds = remainingJoining,
joiningErrorRoomsIds = remainingJoinErrors,
rejectingRoomsIds = remainingRejecting,
rejectingErrorRoomsIds = remainingRejectErrors
)
} }
} }
private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state -> private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state ->
val roomId = action.roomSummary.roomId val roomId = action.roomSummary.roomId
val roomMembershipChange = state.roomMembershipChanges[roomId]
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) { if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen // Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen") Timber.w("Try to join an already joining room. Should not happen")
return@withState return@withState
} }
setState {
copy(
joiningRoomsIds = joiningRoomsIds + roomId,
rejectingErrorRoomsIds = rejectingErrorRoomsIds - roomId
)
}
session.getRoom(roomId)?.join(callback = object : MatrixCallback<Unit> { session.getRoom(roomId)?.join(callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
@ -142,32 +127,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// Notify the user // Notify the user
_viewEvents.post(RoomListViewEvents.Failure(failure)) _viewEvents.post(RoomListViewEvents.Failure(failure))
setState {
copy(
joiningRoomsIds = joiningRoomsIds - roomId,
joiningErrorRoomsIds = joiningErrorRoomsIds + roomId
)
}
} }
}) })
} }
private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state -> private fun handleRejectInvitation(action: RoomListAction.RejectInvitation) = withState { state ->
val roomId = action.roomSummary.roomId val roomId = action.roomSummary.roomId
val roomMembershipChange = state.roomMembershipChanges[roomId]
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) { if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen // Request already sent, should not happen
Timber.w("Try to reject an already rejecting room. Should not happen") Timber.w("Try to left an already leaving or joining room. Should not happen")
return@withState return@withState
} }
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds + roomId,
joiningErrorRoomsIds = joiningErrorRoomsIds - roomId
)
}
session.getRoom(roomId)?.leave(null, object : MatrixCallback<Unit> { session.getRoom(roomId)?.leave(null, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data. // We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
@ -179,12 +151,6 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// Notify the user // Notify the user
_viewEvents.post(RoomListViewEvents.Failure(failure)) _viewEvents.post(RoomListViewEvents.Failure(failure))
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds - roomId,
rejectingErrorRoomsIds = rejectingErrorRoomsIds + roomId
)
}
} }
}) })
} }
@ -235,6 +201,16 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
}) })
} }
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.subscribe {
Timber.v("ChangeMembership states: $it")
setState { copy(roomMembershipChanges = it) }
}
.disposeOnClear()
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries { private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
// Set up init size on directChats and groupRooms as they are the biggest ones // Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>() val invites = ArrayList<RoomSummary>()

View file

@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R import im.vector.riotx.R
@ -30,14 +31,7 @@ data class RoomListViewState(
val asyncRooms: Async<List<RoomSummary>> = Uninitialized, val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val roomFilter: String = "", val roomFilter: String = "",
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized, val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
// List of roomIds that the user wants to join val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
val joiningRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to join
val rejectingRoomsIds: Set<String> = emptySet(),
// List of roomIds that the user wants to reject, but an error occurred
val rejectingErrorRoomsIds: Set<String> = emptySet(),
val isInviteExpanded: Boolean = true, val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true, val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true, val isDirectRoomsExpanded: Boolean = true,

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R import im.vector.riotx.R
@ -72,10 +73,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) } .filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries, buildRoomModels(filteredSummaries,
viewState.joiningRoomsIds, viewState.roomMembershipChanges,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds,
emptySet()) emptySet())
addFilterFooter(viewState) addFilterFooter(viewState)
@ -94,10 +92,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
} }
if (isExpanded) { if (isExpanded) {
buildRoomModels(summaries, buildRoomModels(summaries,
viewState.joiningRoomsIds, viewState.roomMembershipChanges,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds,
emptySet()) emptySet())
// Never set showHelp to true for invitation // Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) { if (category != RoomCategory.INVITE) {
@ -153,18 +148,12 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
} }
private fun buildRoomModels(summaries: List<RoomSummary>, private fun buildRoomModels(summaries: List<RoomSummary>,
joiningRoomsIds: Set<String>, roomChangedMembershipStates: Map<String, ChangeMembershipState>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
selectedRoomIds: Set<String>) { selectedRoomIds: Set<String>) {
summaries.forEach { roomSummary -> summaries.forEach { roomSummary ->
roomSummaryItemFactory roomSummaryItemFactory
.create(roomSummary, .create(roomSummary,
joiningRoomsIds, roomChangedMembershipStates,
joiningErrorRoomsIds,
rejectingRoomsIds,
rejectingErrorRoomsIds,
selectedRoomIds, selectedRoomIds,
listener) listener)
.addTo(this) .addTo(this)

View file

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.list package im.vector.riotx.features.home.room.list
import android.view.View import android.view.View
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
@ -39,23 +40,20 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
private val avatarRenderer: AvatarRenderer) { private val avatarRenderer: AvatarRenderer) {
fun create(roomSummary: RoomSummary, fun create(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>, roomChangeMembershipStates: Map<String, ChangeMembershipState>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
selectedRoomIds: Set<String>, selectedRoomIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) { return when (roomSummary.membership) {
Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener) Membership.INVITE -> {
val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown
createInvitationItem(roomSummary, changeMembershipState, listener)
}
else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked }) else -> createRoomItem(roomSummary, selectedRoomIds, listener?.let { it::onRoomClicked }, listener?.let { it::onRoomLongClicked })
} }
} }
fun createInvitationItem(roomSummary: RoomSummary, private fun createInvitationItem(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>, changeMembershipState: ChangeMembershipState,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) { val secondLine = if (roomSummary.isDirect) {
roomSummary.inviterId roomSummary.inviterId
@ -70,10 +68,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.avatarRenderer(avatarRenderer) .avatarRenderer(avatarRenderer)
.matrixItem(roomSummary.toMatrixItem()) .matrixItem(roomSummary.toMatrixItem())
.secondLine(secondLine) .secondLine(secondLine)
.invitationAcceptInProgress(joiningRoomsIds.contains(roomSummary.roomId)) .changeMembershipState(changeMembershipState)
.invitationAcceptInError(joiningErrorRoomsIds.contains(roomSummary.roomId))
.invitationRejectInProgress(rejectingRoomsIds.contains(roomSummary.roomId))
.invitationRejectInError(rejectingErrorRoomsIds.contains(roomSummary.roomId))
.acceptListener { listener?.onAcceptRoomInvitation(roomSummary) } .acceptListener { listener?.onAcceptRoomInvitation(roomSummary) }
.rejectListener { listener?.onRejectRoomInvitation(roomSummary) } .rejectListener { listener?.onRejectRoomInvitation(roomSummary) }
.listener { listener?.onRoomClicked(roomSummary) } .listener { listener?.onRoomClicked(roomSummary) }

View file

@ -0,0 +1,48 @@
/*
* 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.invite
import androidx.core.view.isInvisible
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.riotx.core.platform.ButtonStateView
object InviteButtonStateBinder {
fun bind(
acceptView: ButtonStateView,
rejectView: ButtonStateView,
changeMembershipState: ChangeMembershipState
) {
// When a request is in progress (accept or reject), we only use the accept State button
// We check for isSuccessful, otherwise we get a glitch the time room summaries get rebuilt
val requestInProgress = changeMembershipState.isInProgress() || changeMembershipState.isSuccessful()
when {
requestInProgress -> acceptView.render(ButtonStateView.State.Loading)
changeMembershipState is ChangeMembershipState.FailedJoining -> acceptView.render(ButtonStateView.State.Error)
else -> acceptView.render(ButtonStateView.State.Button)
}
// ButtonStateView.State.Loaded not used because roomSummary will not be displayed as a room invitation anymore
rejectView.isInvisible = requestInProgress
when {
changeMembershipState is ChangeMembershipState.FailedLeaving -> rejectView.render(ButtonStateView.State.Error)
else -> rejectView.render(ButtonStateView.State.Button)
}
}
}

View file

@ -21,10 +21,12 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.ButtonStateView
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import kotlinx.android.synthetic.main.vector_invite_view.view.* import kotlinx.android.synthetic.main.vector_invite_view.view.*
import javax.inject.Inject import javax.inject.Inject
@ -50,11 +52,28 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
context.injector().inject(this) context.injector().inject(this)
} }
View.inflate(context, R.layout.vector_invite_view, this) View.inflate(context, R.layout.vector_invite_view, this)
inviteRejectView.setOnClickListener { callback?.onRejectInvite() } inviteAcceptView.callback = object : ButtonStateView.Callback {
inviteAcceptView.setOnClickListener { callback?.onAcceptInvite() } override fun onButtonClicked() {
callback?.onAcceptInvite()
}
override fun onRetryClicked() {
callback?.onAcceptInvite()
}
}
inviteRejectView.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() {
callback?.onRejectInvite()
}
override fun onRetryClicked() {
callback?.onRejectInvite()
}
}
} }
fun render(sender: User, mode: Mode = Mode.LARGE) { fun render(sender: User, mode: Mode = Mode.LARGE, changeMembershipState: ChangeMembershipState) {
if (mode == Mode.LARGE) { if (mode == Mode.LARGE) {
updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT } updateLayoutParams { height = LayoutParams.MATCH_CONSTRAINT }
avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView) avatarRenderer.render(sender.toMatrixItem(), inviteAvatarView)
@ -68,5 +87,6 @@ class VectorInviteView @JvmOverloads constructor(context: Context, attrs: Attrib
inviteNameView.visibility = View.GONE inviteNameView.visibility = View.GONE
inviteLabelView.text = context.getString(R.string.invited_by, sender.userId) inviteLabelView.text = context.getString(R.string.invited_by, sender.userId)
} }
InviteButtonStateBinder.bind(inviteAcceptView, inviteRejectView, changeMembershipState)
} }
} }

View file

@ -29,6 +29,7 @@ import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
@ -159,8 +160,8 @@ class DefaultNavigator @Inject constructor(
activity.finish() activity.finish()
} }
override fun openRoomPreview(publicRoom: PublicRoom, context: Context) { override fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData) {
val intent = RoomPreviewActivity.getIntent(context, publicRoom) val intent = RoomPreviewActivity.getIntent(context, publicRoom, roomDirectoryData)
context.startActivity(intent) context.startActivity(intent)
} }

View file

@ -22,6 +22,7 @@ import android.view.View
import androidx.core.util.Pair import androidx.core.util.Pair
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
@ -48,7 +49,7 @@ interface Navigator {
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false) fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
fun openRoomPreview(publicRoom: PublicRoom, context: Context) fun openRoomPreview(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData)
fun openCreateRoom(context: Context, initialName: String = "") fun openCreateRoom(context: Context, initialName: String = "")

View file

@ -21,6 +21,7 @@ import com.airbnb.epoxy.VisibilityState
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
@ -89,13 +90,14 @@ class PublicRoomsController @Inject constructor(private val stringProvider: Stri
roomTopic(publicRoom.topic) roomTopic(publicRoom.topic)
nbOfMembers(publicRoom.numJoinedMembers) nbOfMembers(publicRoom.numJoinedMembers)
val roomChangeMembership = viewState.changeMembershipStates[publicRoom.roomId] ?: ChangeMembershipState.Unknown
val isJoined = viewState.joinedRoomsIds.contains(publicRoom.roomId) || roomChangeMembership is ChangeMembershipState.Joined
val joinState = when { val joinState = when {
viewState.joinedRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINED isJoined -> JoinState.JOINED
viewState.joiningRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING roomChangeMembership is ChangeMembershipState.Joining -> JoinState.JOINING
viewState.joiningErrorRoomsIds.contains(publicRoom.roomId) -> JoinState.JOINING_ERROR roomChangeMembership is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
else -> JoinState.NOT_JOINED else -> JoinState.NOT_JOINED
} }
joinState(joinState) joinState(joinState)
joinListener { joinListener {

View file

@ -114,26 +114,22 @@ class PublicRoomsFragment @Inject constructor(
override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) { override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) {
Timber.v("PublicRoomClicked: $publicRoom") Timber.v("PublicRoomClicked: $publicRoom")
withState(viewModel) { state ->
when (joinState) { when (joinState) {
JoinState.JOINED -> { JoinState.JOINED -> {
navigator.openRoom(requireActivity(), publicRoom.roomId) navigator.openRoom(requireActivity(), publicRoom.roomId)
} }
JoinState.NOT_JOINED, else -> {
JoinState.JOINING_ERROR -> { // ROOM PREVIEW
// ROOM PREVIEW navigator.openRoomPreview(requireActivity(), publicRoom, state.roomDirectoryData)
navigator.openRoomPreview(publicRoom, requireActivity()) }
}
else -> {
Snackbar.make(publicRoomsCoordinator, getString(R.string.please_wait), Snackbar.LENGTH_SHORT)
.show()
} }
} }
} }
override fun onPublicRoomJoin(publicRoom: PublicRoom) { override fun onPublicRoomJoin(publicRoom: PublicRoom) {
Timber.v("PublicRoomJoinClicked: $publicRoom") Timber.v("PublicRoomJoinClicked: $publicRoom")
viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.getPrimaryAlias(), publicRoom.roomId)) viewModel.handle(RoomDirectoryAction.JoinRoom(publicRoom.roomId))
} }
override fun loadMore() { override fun loadMore() {

View file

@ -19,7 +19,9 @@ package im.vector.riotx.features.roomdirectory
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
data class PublicRoomsViewState( data class PublicRoomsViewState(
// The current filter // The current filter
@ -30,11 +32,9 @@ data class PublicRoomsViewState(
val asyncPublicRoomsRequest: Async<List<PublicRoom>> = Uninitialized, val asyncPublicRoomsRequest: Async<List<PublicRoom>> = Uninitialized,
// True if more result are available server side // True if more result are available server side
val hasMore: Boolean = false, val hasMore: Boolean = false,
// Set of roomIds that the user wants to join
val joiningRoomsIds: Set<String> = emptySet(),
// Set of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: Set<String> = emptySet(),
// Set of joined roomId, // Set of joined roomId,
val joinedRoomsIds: Set<String> = emptySet(), val joinedRoomsIds: Set<String> = emptySet(),
val roomDirectoryDisplayName: String? = null // keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(),
val roomDirectoryData: RoomDirectoryData = RoomDirectoryData()
) : MvRxState ) : MvRxState

View file

@ -23,5 +23,5 @@ sealed class RoomDirectoryAction : VectorViewModelAction {
data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction() data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction()
data class FilterWith(val filter: String) : RoomDirectoryAction() data class FilterWith(val filter: String) : RoomDirectoryAction()
object LoadMore : RoomDirectoryAction() object LoadMore : RoomDirectoryAction()
data class JoinRoom(val roomAlias: String?, val roomId: String) : RoomDirectoryAction() data class JoinRoom(val roomId: String) : RoomDirectoryAction()
} }

View file

@ -26,6 +26,7 @@ import com.airbnb.mvrx.appendAt
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
@ -63,18 +64,10 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
private var currentTask: Cancelable? = null private var currentTask: Cancelable? = null
// Default RoomDirectoryData
private var roomDirectoryData = RoomDirectoryData()
init { init {
setState {
copy(
roomDirectoryDisplayName = roomDirectoryData.displayName
)
}
// Observe joined room (from the sync) // Observe joined room (from the sync)
observeJoinedRooms() observeJoinedRooms()
observeMembershipChanges()
} }
private fun observeJoinedRooms() { private fun observeJoinedRooms() {
@ -91,18 +84,21 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
?: emptySet() ?: emptySet()
setState { setState {
copy( copy(joinedRoomsIds = joinedRoomIds)
joinedRoomsIds = joinedRoomIds,
// Remove (newly) joined room id from the joining room list
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) },
// Remove (newly) joined room id from the joining room list in error
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) }
)
} }
} }
.disposeOnClear() .disposeOnClear()
} }
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.subscribe {
setState { copy(changeMembershipStates = it) }
}
.disposeOnClear()
}
override fun handle(action: RoomDirectoryAction) { override fun handle(action: RoomDirectoryAction) {
when (action) { when (action) {
is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action) is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
@ -112,15 +108,15 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
} }
} }
private fun setRoomDirectoryData(action: RoomDirectoryAction.SetRoomDirectoryData) { private fun setRoomDirectoryData(action: RoomDirectoryAction.SetRoomDirectoryData) = withState {
if (this.roomDirectoryData == action.roomDirectoryData) { if (it.roomDirectoryData == action.roomDirectoryData) {
return return@withState
}
setState{
copy(roomDirectoryData = action.roomDirectoryData)
} }
this.roomDirectoryData = action.roomDirectoryData
reset("") reset("")
load("") load("", action.roomDirectoryData)
} }
private fun filterWith(action: RoomDirectoryAction.FilterWith) = withState { state -> private fun filterWith(action: RoomDirectoryAction.FilterWith) = withState { state ->
@ -128,7 +124,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
currentTask?.cancel() currentTask?.cancel()
reset(action.filter) reset(action.filter)
load(action.filter) load(action.filter, state.roomDirectoryData)
} }
} }
@ -141,7 +137,6 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
publicRooms = emptyList(), publicRooms = emptyList(),
asyncPublicRoomsRequest = Loading(), asyncPublicRoomsRequest = Loading(),
hasMore = false, hasMore = false,
roomDirectoryDisplayName = roomDirectoryData.displayName,
currentFilter = newFilter currentFilter = newFilter
) )
} }
@ -154,12 +149,11 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
asyncPublicRoomsRequest = Loading() asyncPublicRoomsRequest = Loading()
) )
} }
load(state.currentFilter, state.roomDirectoryData)
load(state.currentFilter)
} }
} }
private fun load(filter: String) { private fun load(filter: String, roomDirectoryData: RoomDirectoryData) {
currentTask = session.getPublicRooms(roomDirectoryData.homeServer, currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
PublicRoomsParams( PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT, limit = PUBLIC_ROOMS_LIMIT,
@ -204,19 +198,16 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
} }
private fun joinRoom(action: RoomDirectoryAction.JoinRoom) = withState { state -> private fun joinRoom(action: RoomDirectoryAction.JoinRoom) = withState { state ->
if (state.joiningRoomsIds.contains(action.roomId)) { val roomMembershipChange = state.changeMembershipStates[action.roomId]
if (roomMembershipChange?.isInProgress().orFalse()) {
// Request already sent, should not happen // Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen") Timber.w("Try to join an already joining room. Should not happen")
return@withState return@withState
} }
val viaServers = state.roomDirectoryData.homeServer?.let {
setState { listOf(it)
copy( } ?: emptyList()
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(action.roomId) } session.joinRoom(action.roomId, viaServers = viaServers, callback = object : MatrixCallback<Unit> {
)
}
session.joinRoom(action.roomAlias ?: action.roomId, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined // Instead, we wait for the room to be joined
@ -225,20 +216,12 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
// Notify the user // Notify the user
_viewEvents.post(RoomDirectoryViewEvents.Failure(failure)) _viewEvents.post(RoomDirectoryViewEvents.Failure(failure))
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(action.roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(action.roomId) }
)
}
} }
}) })
} }
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
currentTask?.cancel() currentTask?.cancel()
} }
} }

View file

@ -19,5 +19,5 @@ package im.vector.riotx.features.roomdirectory.roompreview
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomPreviewAction : VectorViewModelAction { sealed class RoomPreviewAction : VectorViewModelAction {
data class Join(val roomAlias: String?) : RoomPreviewAction() object Join : RoomPreviewAction()
} }

View file

@ -21,6 +21,7 @@ import android.content.Intent
import android.os.Parcelable import android.os.Parcelable
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.addFragment import im.vector.riotx.core.extensions.addFragment
@ -35,7 +36,8 @@ data class RoomPreviewData(
val roomAlias: String?, val roomAlias: String?,
val topic: String?, val topic: String?,
val worldReadable: Boolean, val worldReadable: Boolean,
val avatarUrl: String? val avatarUrl: String?,
val homeServer: String?
) : Parcelable { ) : Parcelable {
val matrixItem: MatrixItem val matrixItem: MatrixItem
get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl) get() = MatrixItem.RoomItem(roomId, roomName ?: roomAlias, avatarUrl)
@ -46,7 +48,7 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
companion object { companion object {
private const val ARG = "ARG" private const val ARG = "ARG"
fun getIntent(context: Context, publicRoom: PublicRoom): Intent { fun getIntent(context: Context, publicRoom: PublicRoom, roomDirectoryData: RoomDirectoryData): Intent {
return Intent(context, RoomPreviewActivity::class.java).apply { return Intent(context, RoomPreviewActivity::class.java).apply {
putExtra(ARG, RoomPreviewData( putExtra(ARG, RoomPreviewData(
roomId = publicRoom.roomId, roomId = publicRoom.roomId,
@ -54,7 +56,8 @@ class RoomPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
roomAlias = publicRoom.getPrimaryAlias(), roomAlias = publicRoom.getPrimaryAlias(),
topic = publicRoom.topic, topic = publicRoom.topic,
worldReadable = publicRoom.worldReadable, worldReadable = publicRoom.worldReadable,
avatarUrl = publicRoom.avatarUrl avatarUrl = publicRoom.avatarUrl,
homeServer = roomDirectoryData.homeServer
)) ))
} }
} }

View file

@ -65,7 +65,7 @@ class RoomPreviewNoPreviewFragment @Inject constructor(
roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback { roomPreviewNoPreviewJoin.callback = object : ButtonStateView.Callback {
override fun onButtonClicked() { override fun onButtonClicked() {
roomPreviewViewModel.handle(RoomPreviewAction.Join(roomPreviewData.roomAlias)) roomPreviewViewModel.handle(RoomPreviewAction.Join)
} }
override fun onRetryClicked() { override fun onRetryClicked() {

View file

@ -22,7 +22,9 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
@ -32,7 +34,7 @@ import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.roomdirectory.JoinState import im.vector.riotx.features.roomdirectory.JoinState
import timber.log.Timber import timber.log.Timber
class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: RoomPreviewViewState, class RoomPreviewViewModel @AssistedInject constructor(@Assisted private val initialState: RoomPreviewViewState,
private val session: Session) private val session: Session)
: VectorViewModel<RoomPreviewViewState, RoomPreviewAction, EmptyViewEvents>(initialState) { : VectorViewModel<RoomPreviewViewState, RoomPreviewAction, EmptyViewEvents>(initialState) {
@ -52,30 +54,41 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
init { init {
// Observe joined room (from the sync) // Observe joined room (from the sync)
observeJoinedRooms() observeRoomSummary()
observeMembershipChanges()
} }
private fun observeJoinedRooms() { private fun observeRoomSummary() {
val queryParams = roomSummaryQueryParams { val queryParams = roomSummaryQueryParams {
memberships = listOf(Membership.JOIN) roomId = QueryStringValue.Equals(initialState.roomId)
} }
session session
.rx() .rx()
.liveRoomSummaries(queryParams) .liveRoomSummaries(queryParams)
.subscribe { list -> .subscribe { list ->
withState { state -> val isRoomJoined = list.any {
val isRoomJoined = list it.membership == Membership.JOIN
?.map { it.roomId } }
?.toList() if (isRoomJoined) {
?.contains(state.roomId) == true setState { copy(roomJoinState = JoinState.JOINED) }
}
}
.disposeOnClear()
}
if (isRoomJoined) { private fun observeMembershipChanges() {
setState { session.rx()
copy( .liveRoomChangeMembershipState()
roomJoinState = JoinState.JOINED .subscribe {
) val changeMembership = it[initialState.roomId] ?: ChangeMembershipState.Unknown
} val joinState = when (changeMembership) {
} is ChangeMembershipState.Joining -> JoinState.JOINING
is ChangeMembershipState.FailedJoining -> JoinState.JOINING_ERROR
// Other cases are handled by room summary
else -> null
}
if (joinState != null) {
setState { copy(roomJoinState = joinState) }
} }
} }
.disposeOnClear() .disposeOnClear()
@ -83,37 +96,27 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R
override fun handle(action: RoomPreviewAction) { override fun handle(action: RoomPreviewAction) {
when (action) { when (action) {
is RoomPreviewAction.Join -> handleJoinRoom(action) is RoomPreviewAction.Join -> handleJoinRoom()
}.exhaustive }.exhaustive
} }
private fun handleJoinRoom(action: RoomPreviewAction.Join) = withState { state -> private fun handleJoinRoom() = withState { state ->
if (state.roomJoinState == JoinState.JOINING) { if (state.roomJoinState == JoinState.JOINING) {
// Request already sent, should not happen // Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen") Timber.w("Try to join an already joining room. Should not happen")
return@withState return@withState
} }
val viaServers = state.homeServer?.let {
setState { listOf(it)
copy( } ?: emptyList()
roomJoinState = JoinState.JOINING, session.joinRoom(state.roomId, viaServers = viaServers, callback = object : MatrixCallback<Unit> {
lastError = null
)
}
session.joinRoom(action.roomAlias ?: state.roomId, callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
// Instead, we wait for the room to be joined // Instead, we wait for the room to be joined
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
setState { setState { copy(lastError = failure) }
copy(
roomJoinState = JoinState.JOINING_ERROR,
lastError = failure
)
}
} }
}) })
} }

View file

@ -22,11 +22,21 @@ import im.vector.riotx.features.roomdirectory.JoinState
data class RoomPreviewViewState( data class RoomPreviewViewState(
// The room id // The room id
val roomId: String = "", val roomId: String = "",
val roomAlias: String? = null,
/**
* The server name (might be null)
* Set null when the server is the current user's home server.
*/
val homeServer: String? = null,
// Current state of the room in preview // Current state of the room in preview
val roomJoinState: JoinState = JoinState.NOT_JOINED, val roomJoinState: JoinState = JoinState.NOT_JOINED,
// Last error of join room request // Last error of join room request
val lastError: Throwable? = null val lastError: Throwable? = null
) : MvRxState { ) : MvRxState {
constructor(args: RoomPreviewData) : this(roomId = args.roomId) constructor(args: RoomPreviewData) : this(
roomId = args.roomId,
roomAlias = args.roomAlias,
homeServer = args.homeServer
)
} }

View file

@ -112,7 +112,6 @@ class RoomProfileFragment @Inject constructor(
when (it) { when (it) {
is RoomProfileViewEvents.Loading -> showLoading(it.message) is RoomProfileViewEvents.Loading -> showLoading(it.message)
is RoomProfileViewEvents.Failure -> showFailure(it.throwable) is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom()
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog() RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
}.exhaustive }.exhaustive

View file

@ -25,7 +25,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents() data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents() data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
object OnLeaveRoomSuccess : RoomProfileViewEvents()
object OnChangeAvatarSuccess : RoomProfileViewEvents() object OnChangeAvatarSuccess : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents() data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
} }

View file

@ -98,7 +98,7 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
_viewEvents.post(RoomProfileViewEvents.Loading(stringProvider.getString(R.string.room_profile_leaving_room))) _viewEvents.post(RoomProfileViewEvents.Loading(stringProvider.getString(R.string.room_profile_leaving_room)))
room.leave(null, object : MatrixCallback<Unit> { room.leave(null, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
_viewEvents.post(RoomProfileViewEvents.OnLeaveRoomSuccess) // Do nothing, we will be closing the room automatically when it will get back from sync
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {

View file

@ -57,34 +57,35 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inviteIdentifierView" /> app:layout_constraintTop_toBottomOf="@id/inviteIdentifierView" />
<com.google.android.material.button.MaterialButton <im.vector.riotx.core.platform.ButtonStateView
android:id="@+id/inviteRejectView"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:minWidth="120dp"
android:text="@string/reject"
app:layout_constraintEnd_toStartOf="@+id/inviteAcceptView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inviteLabelView" />
<com.google.android.material.button.MaterialButton
android:id="@+id/inviteAcceptView" android:id="@+id/inviteAcceptView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:minWidth="120dp" android:minWidth="120dp"
android:text="@string/accept" app:bsv_button_text="@string/accept"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="false"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/inviteRejectView" app:layout_constraintStart_toEndOf="@+id/inviteRejectView"
app:layout_constraintTop_toTopOf="@id/inviteRejectView" /> app:layout_constraintTop_toTopOf="@id/inviteRejectView" />
<im.vector.riotx.core.platform.ButtonStateView
android:id="@+id/inviteRejectView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:minWidth="120dp"
app:bsv_button_text="@string/reject"
app:bsv_loaded_image_src="@drawable/ic_tick"
app:bsv_use_flat_button="true"
app:layout_constraintEnd_toStartOf="@+id/inviteAcceptView"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/inviteLabelView"/>
<Space <Space
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="16dp" android:layout_height="16dp"

View file

@ -29,6 +29,7 @@
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:layout_gravity="center" android:layout_gravity="center"
android:indeterminateOnly="true"
android:scaleType="center" android:scaleType="center"
tools:layout_gravity="center_horizontal" tools:layout_gravity="center_horizontal"
tools:layout_marginTop="80dp" /> tools:layout_marginTop="80dp" />