/* * Copyright 2019 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package im.vector.riotredesign.features.home.room.detail import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.rx.rx import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.features.command.CommandParser import im.vector.riotredesign.features.command.ParsedCommand import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get import java.util.concurrent.TimeUnit class RoomDetailViewModel(initialState: RoomDetailViewState, private val session: Session, private val visibleRoomHolder: VisibleRoomStore ) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId private val eventId = initialState.eventId private val displayedEventsObservable = BehaviorRelay.create() private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) companion object : MvRxViewModelFactory { const val PAGINATION_COUNT = 50 @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { val currentSession = viewModelContext.activity.get() val visibleRoomHolder = viewModelContext.activity.get() return RoomDetailViewModel(state, currentSession, visibleRoomHolder) } } init { observeRoomSummary() observeEventDisplayedActions() observeInvitationState() room.loadRoomMembersIfNeeded() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } } fun process(action: RoomDetailActions) { when (action) { is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.IsDisplayed -> handleIsDisplayed() is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) is RoomDetailActions.LoadMore -> handleLoadMore(action) is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.RejectInvite -> handleRejectInvite() is RoomDetailActions.RedactAction -> handleRedactEvent(action) is RoomDetailActions.UndoReaction -> handleUndoReact(action) is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) } } private val _sendMessageResultLiveData = MutableLiveData>() val sendMessageResultLiveData: LiveData> get() = _sendMessageResultLiveData // PRIVATE METHODS ***************************************************************************** private fun handleSendMessage(action: RoomDetailActions.SendMessage) { // Handle slash command val slashCommandResult = CommandParser.parseSplashCommand(action.text) when (slashCommandResult) { is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) } is ParsedCommand.ErrorSyntax -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) } is ParsedCommand.ErrorEmptySlashCommand -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) } is ParsedCommand.ErrorUnknownSlashCommand -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) } is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) } is ParsedCommand.SetUserPowerLevel -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.ClearScalarToken -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.SetMarkdown -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.UnbanUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.BanUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.KickUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.JoinRoom -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.PartRoom -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) } is ParsedCommand.ChangeDisplayName -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } } } private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) room.updateTopic(changeTopic.topic, object : MatrixCallback { override fun onSuccess(data: Unit) { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk)) } override fun onFailure(failure: Throwable) { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure))) } }) } private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) room.invite(invite.userId, object : MatrixCallback { override fun onSuccess(data: Unit) { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultOk)) } override fun onFailure(failure: Throwable) { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandResultError(failure))) } }) } private fun handleSendReaction(action: RoomDetailActions.SendReaction) { room.sendReaction(action.reaction, action.targetEventId) } private fun handleRedactEvent(action: RoomDetailActions.RedactAction) { val event = room.getTimeLineEvent(action.targetEventId) ?: return room.redactEvent(event.root, action.reason) } private fun handleUndoReact(action: RoomDetailActions.UndoReaction) { room.undoReaction(action.key, action.targetEventId, session.sessionParams.credentials.userId) } private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) { room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId) } private fun handleSendMedia(action: RoomDetailActions.SendMedia) { val attachments = action.mediaFiles.map { ContentAttachmentData( size = it.size, duration = it.duration, date = it.date, height = it.height, width = it.width, name = it.name, path = it.path, mimeType = it.mimeType, type = ContentAttachmentData.Type.values()[it.mediaType] ) } room.sendMedias(attachments) } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { displayedEventsObservable.accept(action) } private fun handleIsDisplayed() { visibleRoomHolder.post(roomId) } private fun handleLoadMore(action: RoomDetailActions.LoadMore) { timeline.paginate(action.direction, PAGINATION_COUNT) } private fun handleRejectInvite() { room.leave(object : MatrixCallback {}) } private fun handleAcceptInvite() { room.join(object : MatrixCallback {}) } private fun observeEventDisplayedActions() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. displayedEventsObservable .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> val mostRecentEvent = actions.maxBy { it.event.displayIndex } mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) } }) .disposeOnClear() } private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> copy(asyncRoomSummary = async) } } private fun observeInvitationState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> if (summary.membership == Membership.INVITE) { summary.lastMessage?.sender?.let { senderId -> session.getUser(senderId) }?.also { setState { copy(inviter = Success(it)) } } } } } override fun onCleared() { timeline.dispose() super.onCleared() } }