Handle room invitation actions

This commit is contained in:
Benoit Marty 2019-07-01 17:26:24 +02:00 committed by Benoit Marty
parent 07309c90e1
commit 01e3e71f98
12 changed files with 248 additions and 82 deletions

View File

@ -47,7 +47,7 @@ interface MembershipService {
*/
fun getRoomMemberIdsLive(): LiveData<List<String>>
fun getNumberOfJoinedMembers() : Int
fun getNumberOfJoinedMembers(): Int
/**
* Invite a user in the room
@ -55,13 +55,12 @@ interface MembershipService {
fun invite(userId: String, callback: MatrixCallback<Unit>)
/**
* Join the room
* Join the room, or accept an invitation.
*/
fun join(callback: MatrixCallback<Unit>)
/**
* Leave the room.
*
* Leave the room, or reject an invitation.
*/
fun leave(callback: MatrixCallback<Unit>)

View File

@ -17,15 +17,16 @@
package im.vector.riotredesign.features.home.room.list
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.core.platform.ButtonStateView
import im.vector.riotredesign.features.home.AvatarRenderer
@ -38,6 +39,10 @@ abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>(
@EpoxyAttribute var secondLine: CharSequence? = null
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var invitationAcceptInProgress: Boolean = false
@EpoxyAttribute var invitationAcceptInError: Boolean = false
@EpoxyAttribute var invitationRejectInProgress: Boolean = false
@EpoxyAttribute var invitationRejectInError: Boolean = false
@EpoxyAttribute var acceptListener: (() -> Unit)? = null
@EpoxyAttribute var rejectListener: (() -> Unit)? = null
@ -45,8 +50,44 @@ abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>(
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
holder.acceptView.setOnClickListener { acceptListener?.invoke() }
holder.rejectView.setOnClickListener { rejectListener?.invoke() }
// When a request is in progress (accept or reject), we only use the accept State button
val requestInProgress = invitationAcceptInProgress || invitationRejectInProgress
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 = roomName
holder.subtitleView.setTextOrHide(secondLine)
avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
@ -55,8 +96,8 @@ abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>(
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.roomInvitationNameView)
val subtitleView by bind<TextView>(R.id.roomInvitationSubTitle)
val acceptView by bind<Button>(R.id.roomInvitationAccept)
val rejectView by bind<Button>(R.id.roomInvitationReject)
val acceptView by bind<ButtonStateView>(R.id.roomInvitationAccept)
val rejectView by bind<ButtonStateView>(R.id.roomInvitationReject)
val avatarImageView by bind<ImageView>(R.id.roomInvitationAvatarImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomInvitationLayout)
}

View File

@ -24,4 +24,8 @@ sealed class RoomListActions {
data class ToggleCategory(val category: RoomCategory) : RoomListActions()
data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListActions()
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions()
}

View File

@ -24,12 +24,15 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.*
import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.di.ScreenComponent
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.error.ErrorFormatter
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.observeEventDebounced
import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.StateView
@ -64,6 +67,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
private val roomListParams: RoomListParams by args()
@Inject lateinit var roomController: RoomSummaryController
@Inject lateinit var roomListViewModelFactory: RoomListViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
private val roomListViewModel: RoomListViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_room_list
@ -82,6 +86,13 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
}
createChatFabMenu.listener = this
roomListViewModel.invitationAnswerErrorLiveData.observeEvent(this) { throwable ->
vectorBaseActivity.coordinatorLayout?.let {
Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
.show()
}
}
}
private fun setupCreateRoomButton() {
@ -234,11 +245,11 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
}
override fun onAcceptRoomInvitation(room: RoomSummary) {
vectorBaseActivity.notImplemented("Accept room invitation")
roomListViewModel.accept(RoomListActions.AcceptInvitation(room))
}
override fun onRejectRoomInvitation(room: RoomSummary) {
vectorBaseActivity.notImplemented("Reject room invitation")
roomListViewModel.accept(RoomListActions.RejectInvitation(room))
}
override fun onToggleRoomCategory(roomCategory: RoomCategory) {

View File

@ -23,14 +23,18 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
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.session.Session
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.tag.RoomTag
import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.HomeRoomListObservableStore
import timber.log.Timber
class RoomListViewModel @AssistedInject constructor(@Assisted initialState: RoomListViewState,
private val session: Session,
private val homeRoomListObservableSource: HomeRoomListObservableStore,
private val alphabeticalRoomComparator: AlphabeticalRoomComparator,
private val chronologicalRoomComparator: ChronologicalRoomComparator)
@ -56,14 +60,20 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData
private val _invitationAnswerErrorLiveData = MutableLiveData<LiveEvent<Throwable>>()
val invitationAnswerErrorLiveData: LiveData<LiveEvent<Throwable>>
get() = _invitationAnswerErrorLiveData
init {
observeRoomSummaries()
}
fun accept(action: RoomListActions) {
when (action) {
is RoomListActions.SelectRoom -> handleSelectRoom(action)
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
is RoomListActions.SelectRoom -> handleSelectRoom(action)
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
is RoomListActions.AcceptInvitation -> handleAcceptInvitation(action)
is RoomListActions.RejectInvitation -> handleRejectInvitation(action)
}
}
@ -92,6 +102,78 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
}
}
private fun handleAcceptInvitation(action: RoomListActions.AcceptInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
// Request already sent, should not happen
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(roomId) },
rejectingErrorRoomsIds = rejectingErrorRoomsIds.toMutableSet().apply { remove(roomId) }
)
}
session.getRoom(roomId)?.join(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// 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
}
override fun onFailure(failure: Throwable) {
// Notify the user
_invitationAnswerErrorLiveData.postValue(LiveEvent(failure))
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(roomId) }
)
}
}
})
}
private fun handleRejectInvitation(action: RoomListActions.RejectInvitation) = withState { state ->
val roomId = action.roomSummary.roomId
if (state.joiningRoomsIds.contains(roomId) || state.rejectingRoomsIds.contains(roomId)) {
// Request already sent, should not happen
Timber.w("Try to reject an already rejecting room. Should not happen")
return@withState
}
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds.toMutableSet().apply { add(roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { remove(roomId) }
)
}
session.getRoom(roomId)?.leave(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// 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
}
override fun onFailure(failure: Throwable) {
// Notify the user
_invitationAnswerErrorLiveData.postValue(LiveEvent(failure))
setState {
copy(
rejectingRoomsIds = rejectingRoomsIds.toMutableSet().apply { remove(roomId) },
rejectingErrorRoomsIds = rejectingErrorRoomsIds.toMutableSet().apply { add(roomId) }
)
}
}
})
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()

View File

@ -27,6 +27,14 @@ data class RoomListViewState(
val displayMode: RoomListFragment.DisplayMode,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
// List of roomIds that the user wants to join
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 isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true,

View File

@ -39,7 +39,11 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries)
buildRoomModels(summaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
}
}
}
@ -73,10 +77,14 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
}
private fun buildRoomModels(summaries: List<RoomSummary>) {
private fun buildRoomModels(summaries: List<RoomSummary>,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>) {
summaries.forEach { roomSummary ->
roomSummaryItemFactory
.create(roomSummary, listener)
.create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
.addTo(this)
}
}

View File

@ -40,15 +40,24 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) {
fun create(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
fun create(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
Membership.INVITE -> createInvitationItem(roomSummary, listener)
Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
else -> createRoomItem(roomSummary, listener)
}
}
private fun createInvitationItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
private fun createInvitationItem(roomSummary: RoomSummary,
joiningRoomsIds: Set<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.latestEvent?.root?.senderId
} else {
@ -62,6 +71,10 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
.avatarRenderer(avatarRenderer)
.roomId(roomSummary.roomId)
.secondLine(secondLine)
.invitationAcceptInProgress(joiningRoomsIds.contains(roomSummary.roomId))
.invitationAcceptInError(joiningErrorRoomsIds.contains(roomSummary.roomId))
.invitationRejectInProgress(rejectingRoomsIds.contains(roomSummary.roomId))
.invitationRejectInError(rejectingErrorRoomsIds.contains(roomSummary.roomId))
.acceptListener { listener?.onAcceptRoomInvitation(roomSummary) }
.rejectListener { listener?.onRejectRoomInvitation(roomSummary) }
.roomName(roomSummary.displayName)

View File

@ -28,11 +28,11 @@ data class PublicRoomsViewState(
val asyncPublicRoomsRequest: Async<List<PublicRoom>> = Uninitialized,
// True if more result are available server side
val hasMore: Boolean = false,
// List of roomIds that the user wants to join
val joiningRoomsIds: List<String> = emptyList(),
// List of roomIds that the user wants to join, but an error occurred
val joiningErrorRoomsIds: List<String> = emptyList(),
// List of joined roomId,
val joinedRoomsIds: List<String> = emptyList(),
// 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,
val joinedRoomsIds: Set<String> = emptySet(),
val roomDirectoryDisplayName: String? = null
) : MvRxState

View File

@ -18,13 +18,7 @@ package im.vector.riotredesign.features.roomdirectory
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
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.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
@ -95,19 +89,19 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
.liveRoomSummaries()
.subscribe { list ->
val joinedRoomIds = list
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?: emptyList()
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toSet()
?: emptySet()
setState {
copy(
joinedRoomsIds = joinedRoomIds,
// Remove (newly) joined room id from the joining room list
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) },
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) },
// Remove (newly) joined room id from the joining room list in error
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { removeAll(joinedRoomIds) }
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { removeAll(joinedRoomIds) }
)
}
}
@ -166,39 +160,39 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
private fun load() {
currentTask = session.getPublicRooms(roomDirectoryData.homeServer,
PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT,
filter = PublicRoomsFilter(searchTerm = currentFilter),
includeAllNetworks = roomDirectoryData.includeAllNetworks,
since = since,
thirdPartyInstanceId = roomDirectoryData.thirdPartyInstanceId
),
object : MatrixCallback<PublicRoomsResponse> {
override fun onSuccess(data: PublicRoomsResponse) {
currentTask = null
PublicRoomsParams(
limit = PUBLIC_ROOMS_LIMIT,
filter = PublicRoomsFilter(searchTerm = currentFilter),
includeAllNetworks = roomDirectoryData.includeAllNetworks,
since = since,
thirdPartyInstanceId = roomDirectoryData.thirdPartyInstanceId
),
object : MatrixCallback<PublicRoomsResponse> {
override fun onSuccess(data: PublicRoomsResponse) {
currentTask = null
since = data.nextBatch
since = data.nextBatch
setState {
copy(
asyncPublicRoomsRequest = Success(data.chunk!!),
// It's ok to append at the end of the list, so I use publicRooms.size()
publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size),
hasMore = since != null
)
}
}
setState {
copy(
asyncPublicRoomsRequest = Success(data.chunk!!),
// It's ok to append at the end of the list, so I use publicRooms.size()
publicRooms = publicRooms.appendAt(data.chunk!!, publicRooms.size),
hasMore = since != null
)
}
}
override fun onFailure(failure: Throwable) {
currentTask = null
override fun onFailure(failure: Throwable) {
currentTask = null
setState {
copy(
asyncPublicRoomsRequest = Fail(failure)
)
}
}
})
setState {
copy(
asyncPublicRoomsRequest = Fail(failure)
)
}
}
})
}
fun joinRoom(publicRoom: PublicRoom) = withState { state ->
@ -210,7 +204,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { add(publicRoom.roomId) }
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { add(publicRoom.roomId) }
)
}
@ -226,8 +220,8 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
setState {
copy(
joiningRoomsIds = joiningRoomsIds.toMutableList().apply { remove(publicRoom.roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableList().apply { add(publicRoom.roomId) }
joiningRoomsIds = joiningRoomsIds.toMutableSet().apply { remove(publicRoom.roomId) },
joiningErrorRoomsIds = joiningErrorRoomsIds.toMutableSet().apply { add(publicRoom.roomId) }
)
}
}

View File

@ -7,7 +7,7 @@
tools:openDrawer="start">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:id="@+id/vector_coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@ -77,23 +77,29 @@
app:layout_constraintTop_toBottomOf="@+id/roomInvitationSubTitle"
tools:layout_marginStart="120dp" />
<com.google.android.material.button.MaterialButton
<im.vector.riotredesign.core.platform.ButtonStateView
android:id="@+id/roomInvitationAccept"
style="@style/VectorButtonStyle"
android:layout_marginTop="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:minWidth="122dp"
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="@+id/roomInvitationNameView"
app:layout_constraintTop_toBottomOf="@+id/roomLastEventBottomSpace" />
<com.google.android.material.button.MaterialButton
<im.vector.riotredesign.core.platform.ButtonStateView
android:id="@+id/roomInvitationReject"
style="@style/VectorButtonStyleOutlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_vertical_margin"
android:minWidth="122dp"
android:text="@string/reject"
android:textAllCaps="true"
android:textColor="?riotx_text_primary"
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/roomInvitationAccept"
app:layout_constraintTop_toTopOf="@+id/roomInvitationAccept" />