Toggle Quick React (agree/disagree like/dislike)

This commit is contained in:
Valere 2019-05-20 12:43:02 +02:00
parent 44d1d063e9
commit 5dfc0b3c0e
13 changed files with 214 additions and 72 deletions

View File

@ -35,4 +35,12 @@ interface ReactionService {
*/ */
fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable



/**
* Undo a reaction (emoji) to the targetedEvent.
* @param reaction the reaction (preferably emoji)
* @param targetEventId the id of the event being reacted
*/
fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String)

} }

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.annotation.DefaultReactionService import im.vector.matrix.android.internal.session.room.annotation.DefaultReactionService
import im.vector.matrix.android.internal.session.room.annotation.UpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
@ -48,6 +49,7 @@ internal class RoomFactory(private val monarchy: Monarchy,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val setReadMarkersTask: SetReadMarkersTask, private val setReadMarkersTask: SetReadMarkersTask,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val updateQuickReactionTask: UpdateQuickReactionTask,
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask) { private val leaveRoomTask: LeaveRoomTask) {


@ -56,7 +58,7 @@ internal class RoomFactory(private val monarchy: Monarchy,
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask)
val sendService = DefaultSendService(roomId, eventFactory, monarchy) val sendService = DefaultSendService(roomId, eventFactory, monarchy)
val reactionService = DefaultReactionService(roomId, eventFactory, monarchy, findReactionEventForUndoTask, taskExecutor) val reactionService = DefaultReactionService(roomId, eventFactory, findReactionEventForUndoTask, updateQuickReactionTask, taskExecutor)
val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask)
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask)

View File

@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.session.room


import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.annotation.DefaultFindReactionEventForUndoTask import im.vector.matrix.android.internal.session.room.annotation.DefaultFindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.annotation.DefaultUpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.annotation.UpdateQuickReactionTask
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.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
@ -75,7 +77,7 @@ class RoomModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
@ -102,6 +104,10 @@ class RoomModule {
DefaultFindReactionEventForUndoTask(get()) as FindReactionEventForUndoTask DefaultFindReactionEventForUndoTask(get()) as FindReactionEventForUndoTask
} }


scope(DefaultSession.SCOPE) {
DefaultUpdateQuickReactionTask(get()) as UpdateQuickReactionTask
}

scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultPruneEventTask(get()) as PruneEventTask DefaultPruneEventTask(get()) as PruneEventTask
} }

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.room.annotation package im.vector.matrix.android.internal.session.room.annotation


import androidx.work.* import androidx.work.*
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
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.model.annotation.ReactionService import im.vector.matrix.android.api.session.room.model.annotation.ReactionService
@ -39,8 +38,8 @@ private val WORK_CONSTRAINTS = Constraints.Builder()


internal class DefaultReactionService(private val roomId: String, internal class DefaultReactionService(private val roomId: String,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val monarchy: Monarchy,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val updateQuickReactionTask: UpdateQuickReactionTask,
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor)
: ReactionService { : ReactionService {


@ -95,6 +94,32 @@ internal class DefaultReactionService(private val roomId: String,


} }



override fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) {

val params = UpdateQuickReactionTask.Params(
roomId,
targetEventId,
reaction,
oppositeReaction,
myUserId
)

updateQuickReactionTask.configureWith(params)
.dispatchTo(object : MatrixCallback<UpdateQuickReactionTask.Result> {
override fun onSuccess(data: UpdateQuickReactionTask.Result) {
data.reactionToAdd?.also { sendReaction(it, targetEventId) }
data.reactionToRedact.forEach {
val redactWork = createRedactEventWork(it, null)
WorkManager.getInstance()
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork)
.enqueue()
}
}
})
.executeBy(taskExecutor)
}

private fun buildWorkIdentifier(identifier: String): String { private fun buildWorkIdentifier(identifier: String): String {
return "${roomId}_$identifier" return "${roomId}_$identifier"
} }

View File

@ -19,6 +19,7 @@ import android.content.Context
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent
@ -66,6 +67,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters)
content = localEvent.content content = localEvent.content
) )
} }
return result.fold({ Result.retry() }, { Result.success() }) return result.fold({
when (it) {
is Failure.NetworkConnection -> Result.retry()
else -> Result.failure()
}
}, { Result.success() })
} }
} }

View File

@ -0,0 +1,89 @@
/*
* 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.matrix.android.internal.session.room.annotation

import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm


internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params, UpdateQuickReactionTask.Result> {

data class Params(
val roomId: String,
val eventId: String,
val reaction: String,
val oppositeReaction: String,
val myUserId: String
)

data class Result(
val reactionToAdd: String?,
val reactionToRedact: List<String>
)
}

internal class DefaultUpdateQuickReactionTask(private val monarchy: Monarchy) : UpdateQuickReactionTask {
override fun execute(params: UpdateQuickReactionTask.Params): Try<UpdateQuickReactionTask.Result> {
return Try {
var res: Pair<String?, List<String>?>? = null
monarchy.doWithRealm { realm ->
res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId)
}
UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList())
}
}


private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair<String?, List<String>?> {
//the emoji reaction has been selected, we need to check if we have reacted it or not
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
?: return Pair(reaction, null)

//Ok there is already reactions on this event, have we reacted to it
val aggregationForReaction = existingSummary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
.findFirst()
val aggregationForOppositeReaction = existingSummary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, oppositeReaction)
.findFirst()

if (aggregationForReaction == null || !aggregationForReaction.addedByMe) {
//i haven't yet reacted to it, so need to add it, but do I need to redact the opposite?
val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
//find source event
val entity = EventEntity.where(realm, it).findFirst()
if (entity?.sender == myUserId) entity.eventId else null
}
return Pair(reaction, toRedact)
} else {
//I already added it, so i need to undo it (like a toggle)
// find all m.redaction coming from me to readact them
val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
//find source event
val entity = EventEntity.where(realm, it).findFirst()
if (entity?.sender == myUserId) entity.eventId else null
}
return Pair(null, toRedact)
}

}
}

View File

@ -30,6 +30,7 @@ sealed class RoomDetailActions {
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String,val selectedReaction: String,val opposite: String) : RoomDetailActions()
object AcceptInvite : RoomDetailActions() object AcceptInvite : RoomDetailActions()
object RejectInvite : RoomDetailActions() object RejectInvite : RoomDetailActions()



View File

@ -54,11 +54,7 @@ import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
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.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R import im.vector.riotredesign.R
@ -69,15 +65,7 @@ import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
import im.vector.riotredesign.core.utils.checkPermissions
import im.vector.riotredesign.core.utils.copyToClipboard
import im.vector.riotredesign.core.utils.openCamera
import im.vector.riotredesign.core.utils.shareMedia
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
@ -199,11 +187,11 @@ class RoomDetailFragment :
if (resultCode == RESULT_OK && data != null) { if (resultCode == RESULT_OK && data != null) {
when (requestCode) { when (requestCode) {
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return ?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return ?: return
//TODO check if already reacted with that? //TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
} }
@ -367,24 +355,24 @@ class RoomDetailFragment :
private fun onSendChoiceClicked(dialogListItem: DialogListItem) { private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
Timber.v("On send choice clicked: $dialogListItem") Timber.v("On send choice clicked: $dialogListItem")
when (dialogListItem) { when (dialogListItem) {
is DialogListItem.SendFile -> { is DialogListItem.SendFile -> {
// launchFileIntent // launchFileIntent
} }
is DialogListItem.SendVoice -> { is DialogListItem.SendVoice -> {
//launchAudioRecorderIntent() //launchAudioRecorderIntent()
} }
is DialogListItem.SendSticker -> { is DialogListItem.SendSticker -> {
//startStickerPickerActivity() //startStickerPickerActivity()
} }
is DialogListItem.TakePhotoVideo -> is DialogListItem.TakePhotoVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
// launchCamera() // launchCamera()
} }
is DialogListItem.TakePhoto -> is DialogListItem.TakePhoto ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE) openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
} }
is DialogListItem.TakeVideo -> is DialogListItem.TakeVideo ->
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
// launchNativeVideoRecorder() // launchNativeVideoRecorder()
} }
@ -431,20 +419,20 @@ class RoomDetailFragment :
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) { when (sendMessageResult) {
is SendMessageResult.MessageSent, is SendMessageResult.MessageSent,
is SendMessageResult.SlashCommandHandled -> { is SendMessageResult.SlashCommandHandled -> {
// Clear composer // Clear composer
composerEditText.text = null composerEditText.text = null
} }
is SendMessageResult.SlashCommandError -> { is SendMessageResult.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
} }
is SendMessageResult.SlashCommandUnknown -> { is SendMessageResult.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
} }
is SendMessageResult.SlashCommandResultOk -> { is SendMessageResult.SlashCommandResultOk -> {
// Ignore // Ignore
} }
is SendMessageResult.SlashCommandResultError -> { is SendMessageResult.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage) displayCommandError(sendMessageResult.throwable.localizedMessage)
} }
is SendMessageResult.SlashCommandNotImplemented -> { is SendMessageResult.SlashCommandNotImplemented -> {
@ -522,7 +510,7 @@ class RoomDetailFragment :
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId))
} else { } else {
//I need to redact a reaction //I need to redact a reaction
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId,reaction)) roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
} }
} }


@ -536,11 +524,11 @@ class RoomDetailFragment :
it?.getContentIfNotHandled()?.let { actionData -> it?.getContentIfNotHandled()?.let { actionData ->


when (actionData.actionId) { when (actionData.actionId) {
MessageMenuViewModel.ACTION_ADD_REACTION -> { MessageMenuViewModel.ACTION_ADD_REACTION -> {
val eventId = actionData.data?.toString() ?: return val eventId = actionData.data?.toString() ?: return
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE) startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE)
} }
MessageMenuViewModel.ACTION_COPY -> { MessageMenuViewModel.ACTION_COPY -> {
//I need info about the current selected message :/ //I need info about the current selected message :/
copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false) copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false)
val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
@ -549,7 +537,7 @@ class RoomDetailFragment :
} }
MessageMenuViewModel.ACTION_DELETE -> { MessageMenuViewModel.ACTION_DELETE -> {
val eventId = actionData.data?.toString() ?: return val eventId = actionData.data?.toString() ?: return
roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId,context?.getString(R.string.event_redacted_by_user_reason))) roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason)))
} }
MessageMenuViewModel.ACTION_SHARE -> { MessageMenuViewModel.ACTION_SHARE -> {
//TODO current data communication is too limited //TODO current data communication is too limited
@ -594,12 +582,13 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
.show() .show()
} }
MessageMenuViewModel.ACTION_QUICK_REACT -> { MessageMenuViewModel.ACTION_QUICK_REACT -> {
(actionData.data as? Pair<String, String>)?.let { pairData -> //eventId,ClickedOn,Opposite
roomDetailViewModel.process(RoomDetailActions.SendReaction(pairData.second, pairData.first)) (actionData.data as? Triple<String, String, String>)?.let { (eventId, clickedOn, opposite) ->
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite))
} }
} }
else -> { else -> {
Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
} }
} }

View File

@ -72,18 +72,21 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,


fun process(action: RoomDetailActions) { fun process(action: RoomDetailActions) {
when (action) { when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> handleIsDisplayed() is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.SendMedia -> handleSendMedia(action)
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
is RoomDetailActions.LoadMore -> handleLoadMore(action) is RoomDetailActions.LoadMore -> handleLoadMore(action)
is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.SendReaction -> handleSendReaction(action)
is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.AcceptInvite -> handleAcceptInvite()
is RoomDetailActions.RejectInvite -> handleRejectInvite() is RoomDetailActions.RejectInvite -> handleRejectInvite()
is RoomDetailActions.RedactAction -> handleRedactEvent(action) is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
} }
} }



private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>() private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>> val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
get() = _sendMessageResultLiveData get() = _sendMessageResultLiveData
@ -95,63 +98,63 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
val slashCommandResult = CommandParser.parseSplashCommand(action.text) val slashCommandResult = CommandParser.parseSplashCommand(action.text)


when (slashCommandResult) { when (slashCommandResult) {
is ParsedCommand.ErrorNotACommand -> { is ParsedCommand.ErrorNotACommand -> {
// Send the text message to the room // Send the text message to the room
room.sendTextMessage(action.text) room.sendTextMessage(action.text)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent))
} }
is ParsedCommand.ErrorSyntax -> { is ParsedCommand.ErrorSyntax -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command)))
} }
is ParsedCommand.ErrorEmptySlashCommand -> { is ParsedCommand.ErrorEmptySlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/")))
} }
is ParsedCommand.ErrorUnknownSlashCommand -> { is ParsedCommand.ErrorUnknownSlashCommand -> {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand)))
} }
is ParsedCommand.Invite -> { is ParsedCommand.Invite -> {
handleInviteSlashCommand(slashCommandResult) handleInviteSlashCommand(slashCommandResult)
} }
is ParsedCommand.SetUserPowerLevel -> { is ParsedCommand.SetUserPowerLevel -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.ClearScalarToken -> { is ParsedCommand.ClearScalarToken -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.SetMarkdown -> { is ParsedCommand.SetMarkdown -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.UnbanUser -> { is ParsedCommand.UnbanUser -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.BanUser -> { is ParsedCommand.BanUser -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.KickUser -> { is ParsedCommand.KickUser -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.JoinRoom -> { is ParsedCommand.JoinRoom -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.PartRoom -> { is ParsedCommand.PartRoom -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
is ParsedCommand.SendEmote -> { is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
} }
is ParsedCommand.ChangeTopic -> { is ParsedCommand.ChangeTopic -> {
handleChangeTopicSlashCommand(slashCommandResult) handleChangeTopicSlashCommand(slashCommandResult)
} }
is ParsedCommand.ChangeDisplayName -> { is ParsedCommand.ChangeDisplayName -> {
// TODO // TODO
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented))
} }
@ -201,6 +204,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }




private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId)
}


private fun handleSendMedia(action: RoomDetailActions.SendMedia) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.mediaFiles.map { val attachments = action.mediaFiles.map {
ContentAttachmentData( ContentAttachmentData(
@ -238,6 +246,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
room.join(object : MatrixCallback<Unit> {}) room.join(object : MatrixCallback<Unit> {})
} }



private fun observeEventDisplayedActions() { private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second // We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on. // and keep the most recent one to set the read receipt on.

View File

@ -95,13 +95,8 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
.commit() .commit()
} }
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
override fun didQuickReactWith(clikedOn: String, reactions: List<String>, eventId: String) { override fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String) {
if (reactions.contains(clikedOn)) { actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, opposite))
//it's an add
actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Pair(eventId,clikedOn))
} else {
//it's a remove
}
dismiss() dismiss()
} }
} }

View File

@ -119,12 +119,14 @@ class QuickReactionFragment : BaseMvRxFragment() {
} }


if (it.selectionResult != null) { if (it.selectionResult != null) {
interactionListener?.didQuickReactWith(it.selectionResult.first, it.selectionResult.second, it.eventId) val clikedOn = it.selectionResult.first
interactionListener?.didQuickReactWith(clikedOn, QuickReactionViewModel.getOpposite(clikedOn)
?: "", it.selectionResult.second, it.eventId)
} }
} }


interface InteractionListener { interface InteractionListener {
fun didQuickReactWith(clikedOn: String, reactions: List<String>, eventId: String) fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List<String>, eventId: String)
} }


companion object { companion object {

View File

@ -110,6 +110,16 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
val likePositive = "🙂" val likePositive = "🙂"
val likeNegative = "😔" val likeNegative = "😔"


fun getOpposite(reaction: String): String? {
return when (reaction) {
agreePositive -> agreeNegative
agreeNegative -> agreePositive
likePositive -> likeNegative
likeNegative -> likePositive
else -> null
}
}

override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? { override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? {
// Args are accessible from the context. // Args are accessible from the context.
// val foo = vieWModelContext.args<MyArgs>.foo // val foo = vieWModelContext.args<MyArgs>.foo

View File

@ -125,7 +125,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
holder.reactionFlowHelper?.requestLayout() holder.reactionFlowHelper?.requestLayout()
} }

holder.reactionWrapper?.setOnLongClickListener(longClickListener)
} }
} }