diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 6a0d672ce6..7cd2dbf099 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -49,4 +49,6 @@ interface SendService { fun sendMedias(attachments: List): Cancelable + fun sendReaction(reaction: String, targetEventId: String) : Cancelable + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index c3617504d9..e24a6e0665 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.internal.network.NetworkConstants @@ -156,4 +157,20 @@ internal interface RoomAPI { @Path("state_event_type") stateEventType: String, @Path("state_key") stateKey: String, @Body params: Map): Call + + /** + * Send a relation event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}") + fun sendRelation(@Path("roomId") roomId: String, + @Path("parentId") parent_id: String, + @Path("relation_type") relationType: String, + @Path("eventType") eventType: String, + @Body content: Content? + ): Call } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 0089844c45..916077dff7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -74,6 +74,18 @@ internal class DefaultSendService(private val roomId: String, return cancelableBag } + + override fun sendReaction(reaction: String, targetEventId: String) : Cancelable { + val event = eventFactory.createReactionEvent(roomId,targetEventId,reaction).also { + saveLocalEcho(it) + } + val sendRelationWork = createSendRelationWork(event) + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendRelationWork) + .enqueue() + return CancelableWork(sendRelationWork.id) + } + override fun sendMedia(attachment: ContentAttachmentData): Cancelable { // Create an event with the media file path val event = eventFactory.createMediaEvent(roomId, attachment).also { @@ -116,6 +128,19 @@ internal class DefaultSendService(private val roomId: String, .build() } + private fun createSendRelationWork(event: Event): OneTimeWorkRequest { + //TODO use the new API to send relation (for now use regular send) + val sendContentWorkerParams = SendEventWorker.Params( + roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(sendWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { val uploadMediaWorkerParams = UploadContentWorker.Params(roomId, event, attachment) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 778a49eca4..a2076d3b62 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -21,18 +21,11 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toContent -import im.vector.matrix.android.api.session.room.model.message.AudioInfo -import im.vector.matrix.android.api.session.room.model.message.FileInfo -import im.vector.matrix.android.api.session.room.model.message.ImageInfo -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -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.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent -import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo -import im.vector.matrix.android.api.session.room.model.message.VideoInfo +import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent +import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.session.content.ThumbnailExtractor internal class LocalEchoEventFactory(private val credentials: Credentials) { @@ -47,10 +40,29 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment) - ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) } } + fun createReactionEvent(roomId: String, targetEventId: String, reaction: String): Event { + val content = ReactionContent( + ReactionInfo( + RelationType.ANNOTATION, + targetEventId, + reaction + ) + ) + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + sender = credentials.userId, + eventId = dummyEventId(roomId), + type = EventType.REACTION, + content = content.toContent() + ) + } + + private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event { val content = MessageImageContent( type = MessageType.MSGTYPE_IMAGE, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendRelationWorker.kt new file mode 100644 index 0000000000..96de9f0fb8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendRelationWorker.kt @@ -0,0 +1,55 @@ +package im.vector.matrix.android.internal.session.room.send + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +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.room.model.annotation.ReactionContent +import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo +import im.vector.matrix.android.internal.di.MatrixKoinComponent +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.util.WorkerParamsFactory +import org.koin.standalone.inject + +class SendRelationWorker(context: Context, params: WorkerParameters) + : Worker(context, params), MatrixKoinComponent { + + + @JsonClass(generateAdapter = true) + internal data class Params( + val roomId: String, + val event: Event, + val relationType: String? = null + ) + + private val roomAPI by inject() + + override fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + + val localEvent = params.event + if (localEvent.eventId == null) { + return Result.failure() + } + val relationContent = localEvent.content.toModel() + ?: return Result.failure() + val relatedEventId = relationContent.relatesTo?.eventId ?: return Result.failure() + val relationType = (relationContent.relatesTo as? ReactionInfo)?.type ?: params.relationType + ?: return Result.failure() + + val result = executeRequest { + apiCall = roomAPI.sendRelation( + roomId = params.roomId, + parent_id = relatedEventId, + relationType = relationType, + eventType = localEvent.type, + content = localEvent.content + ) + } + return result.fold({ Result.retry() }, { Result.success() }) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index f7297f273a..98a1ab305c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -419,6 +419,8 @@ internal class DefaultTimeline( val timelineEvent = timelineEventFactory.create(eventEntity) val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size builtEvents.add(position, timelineEvent) + //Need to shift :/ + builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) } builtEventsIdMap[eventEntity.eventId] = position } Timber.v("Built ${offsetResults.size} items from db") diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 38c436b512..e1ddcee949 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -28,4 +28,8 @@ sealed class RoomDetailActions { data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() + data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() + + + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 507bf4cd93..49e4fd62ff 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -107,6 +107,7 @@ data class RoomDetailArgs( private const val CAMERA_VALUE_TITLE = "attachment" private const val REQUEST_FILES_REQUEST_CODE = 0 private const val TAKE_IMAGE_REQUEST_CODE = 1 +private const val REACTION_SELECT_REQUEST_CODE = 2 class RoomDetailFragment : VectorBaseFragment(), @@ -182,6 +183,12 @@ class RoomDetailFragment : if (resultCode == RESULT_OK && data != null) { when (requestCode) { REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) + REACTION_SELECT_REQUEST_CODE -> { + val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) ?: return + val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return + //TODO check if already reacted with that? + roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction,eventId)) + } } } } @@ -476,6 +483,16 @@ class RoomDetailFragment : override fun onMemberNameClicked(informationData: MessageInformationData) { insertUserDisplayNameInTextEditor(informationData.memberName?.toString()) } + + override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) { + if (on) { + //we should test the current real state of reaction on this event + roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction,informationData.eventId)) + } else { + //TODO it's an undo :/ + } + } + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { @@ -487,7 +504,8 @@ class RoomDetailFragment : when (actionData.actionId) { MessageMenuViewModel.ACTION_ADD_REACTION -> { - startActivityForResult(EmojiReactionPickerActivity.intent(requireContext()), 0) + val eventId = actionData.data?.toString() ?: return + startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE) } MessageMenuViewModel.ACTION_COPY -> { //I need info about the current selected message :/ @@ -539,6 +557,11 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } .show() } + MessageMenuViewModel.ACTION_QUICK_REACT -> { + (actionData.data as? Pair)?.let { pairData -> + roomDetailViewModel.process(RoomDetailActions.SendReaction(pairData.second, pairData.first)) + } + } else -> { Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() } @@ -553,6 +576,7 @@ class RoomDetailFragment : * @param text the text to insert. */ private fun insertUserDisplayNameInTextEditor(text: String?) { + //TODO move logic outside of fragment if (null != text) { // var vibrate = false diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index d2662d7938..553078b8c6 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -69,11 +69,12 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.IsDisplayed -> handleIsDisplayed() - is RoomDetailActions.SendMedia -> handleSendMedia(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.LoadMore -> handleLoadMore(action) + is RoomDetailActions.SendReaction -> handleSendReaction(action) } } @@ -88,63 +89,63 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, val slashCommandResult = CommandParser.parseSplashCommand(action.text) when (slashCommandResult) { - is ParsedCommand.ErrorNotACommand -> { + is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) } - is ParsedCommand.ErrorSyntax -> { + is ParsedCommand.ErrorSyntax -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) } - is ParsedCommand.ErrorEmptySlashCommand -> { + is ParsedCommand.ErrorEmptySlashCommand -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) } is ParsedCommand.ErrorUnknownSlashCommand -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) } - is ParsedCommand.Invite -> { + is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) } - is ParsedCommand.SetUserPowerLevel -> { + is ParsedCommand.SetUserPowerLevel -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.ClearScalarToken -> { + is ParsedCommand.ClearScalarToken -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.SetMarkdown -> { + is ParsedCommand.SetMarkdown -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.UnbanUser -> { + is ParsedCommand.UnbanUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.BanUser -> { + is ParsedCommand.BanUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.KickUser -> { + is ParsedCommand.KickUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.JoinRoom -> { + is ParsedCommand.JoinRoom -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.PartRoom -> { + is ParsedCommand.PartRoom -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.SendEmote -> { + is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) } - is ParsedCommand.ChangeTopic -> { + is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) } - is ParsedCommand.ChangeDisplayName -> { + is ParsedCommand.ChangeDisplayName -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } @@ -179,6 +180,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, }) } + + private fun handleSendReaction(action: RoomDetailActions.SendReaction) { + room.sendReaction(action.reaction,action.targetEventId) + } + private fun handleSendMedia(action: RoomDetailActions.SendMedia) { val attachments = action.mediaFiles.map { ContentAttachmentData( diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index c40511e8d6..4674edd493 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -47,7 +47,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { - interface Callback { + interface Callback : ReactionPillCallback { fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) @@ -60,6 +60,10 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, fun onMemberNameClicked(informationData: MessageInformationData) } + interface ReactionPillCallback { + fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) + } + private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() private val modelCache = arrayListOf() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index 180bacc09f..c1576eef11 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -95,8 +95,13 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { .commit() } quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { - override fun didQuickReactWith(reactions: List) { - actionHandlerModel.fireAction("Quick React", reactions) + override fun didQuickReactWith(clikedOn: String, reactions: List, eventId: String) { + if (reactions.contains(clikedOn)) { + //it's an add + actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Pair(eventId,clikedOn)) + } else { + //it's a remove + } dismiss() } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index ebe02f122c..9f52306272 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -67,7 +67,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel().apply { - this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_smile)) + this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_smile, event.root.eventId)) if (canCopy(type)) { //TODO copy images? html? see ClipBoard this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body)) @@ -184,6 +184,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel) + fun didQuickReactWith(clikedOn: String, reactions: List, eventId: String) } companion object { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt index 2bc1a8dd55..8284c16c6b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt @@ -31,7 +31,12 @@ enum class TriggleState { SECOND } -data class QuickReactionState(val agreeTrigleState: TriggleState, val likeTriggleState: TriggleState, val selectionResult: List? = null) : MvRxState +data class QuickReactionState( + val agreeTrigleState: TriggleState, + val likeTriggleState: TriggleState, + /** Pair of 'clickedOn' and current toggles state*/ + val selectionResult: Pair>? = null, + val eventId: String) : MvRxState /** * Quick reaction view model @@ -43,16 +48,18 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel fun toggleAgree(isFirst: Boolean) = withState { if (isFirst) { setState { + val newTriggle = if (it.agreeTrigleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST copy( - agreeTrigleState = if (it.agreeTrigleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST, - selectionResult = getReactions(this) + agreeTrigleState = newTriggle, + selectionResult = Pair(agreePositive, getReactions(this, newTriggle, null)) ) } } else { setState { + val newTriggle = if (it.agreeTrigleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND copy( - agreeTrigleState = if (it.agreeTrigleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND, - selectionResult = getReactions(this) + agreeTrigleState = agreeTrigleState, + selectionResult = Pair(agreeNegative, getReactions(this, newTriggle, null)) ) } } @@ -61,30 +68,32 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel fun toggleLike(isFirst: Boolean) = withState { if (isFirst) { setState { + val newTriggle = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST copy( - likeTriggleState = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST, - selectionResult = getReactions(this) + likeTriggleState = newTriggle, + selectionResult = Pair(likePositive, getReactions(this, null, newTriggle)) ) } } else { setState { + val newTriggle = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND copy( - likeTriggleState = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND, - selectionResult = getReactions(this) + likeTriggleState = newTriggle, + selectionResult = Pair(likeNegative, getReactions(this, null, newTriggle)) ) } } } - private fun getReactions(state: QuickReactionState): List { + private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List { return ArrayList(4).apply { - when (state.likeTriggleState) { + when (newState2 ?: state.likeTriggleState) { TriggleState.FIRST -> add(likePositive) TriggleState.SECOND -> add(likeNegative) else -> { } } - when (state.agreeTrigleState) { + when (newState1 ?: state.agreeTrigleState) { TriggleState.FIRST -> add(agreePositive) TriggleState.SECOND -> add(agreeNegative) else -> { @@ -126,7 +135,7 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel } } } - return QuickReactionState(agreeTriggle, likeTriggle) + return QuickReactionState(agreeTriggle, likeTriggle, null, event.root.eventId ?: "") } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 8afdf200a8..924c34813d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -107,6 +107,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .informationData(informationData) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) + .reactionPillCallback(callback) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -134,6 +135,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageFileItem_() .informationData(informationData) .filename(messageContent.body) + .reactionPillCallback(callback) .iconRes(R.drawable.filetype_attachment) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> @@ -180,6 +182,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .playable(messageContent.info?.mimeType == "image/gif") .informationData(informationData) .mediaData(data) + .reactionPillCallback(callback) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -226,6 +229,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .playable(true) .informationData(informationData) .mediaData(thumbnailData) + .reactionPillCallback(callback) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -257,6 +261,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(linkifiedBody) .informationData(informationData) + .reactionPillCallback(callback) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -294,6 +299,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(message) .informationData(informationData) + .reactionPillCallback(callback) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) @@ -322,6 +328,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return MessageTextItem_() .message(message) .informationData(informationData) + .reactionPillCallback(callback) .avatarClickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onAvatarClicked(informationData) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index 045bb1c76d..6db0e0eb39 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -30,6 +30,7 @@ import com.airbnb.epoxy.EpoxyAttribute import im.vector.riotredesign.R import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx import im.vector.riotredesign.features.home.AvatarRenderer +import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.reactions.widget.ReactionButton @@ -49,6 +50,19 @@ abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute var memberClickListener: View.OnClickListener? = null + @EpoxyAttribute + var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null + + var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { + override fun onReacted(reactionButton: ReactionButton) { + reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString,true) + } + + override fun onUnReacted(reactionButton: ReactionButton) { + reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString,false) + } + } + override fun bind(holder: H) { super.bind(holder) if (informationData.showInformation) { @@ -88,9 +102,11 @@ abstract class AbsMessageItem : BaseEventItem() { //clear all reaction buttons (but not the Flow helper!) holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } val idToRefInFlow = ArrayList() - informationData.orderedReactionList?.forEachIndexed { index, reaction -> - (holder.reactionWrapper?.children?.elementAt(index) as? ReactionButton)?.let { reactionButton -> + informationData.orderedReactionList?.chunked(7)?.firstOrNull()?.forEachIndexed { index, reaction -> + (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton -> reactionButton.isVisible = true + reactionButton.reactedListener = reactionClickListener + reactionButton.setTag(R.id.messageBottomInfo, reaction.first) idToRefInFlow.add(reactionButton.id) reactionButton.reactionString = reaction.first reactionButton.reactionCount = reaction.second @@ -107,6 +123,10 @@ abstract class AbsMessageItem : BaseEventItem() { } } + override fun unbind(holder: H) { + super.unbind(holder) + } + protected fun View.renderSendState() { isClickable = informationData.sendState.isSent() alpha = if (informationData.sendState.isSent()) 1f else 0.5f diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiChooserViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiChooserViewModel.kt index e1bd7c9018..7b11adcd38 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiChooserViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiChooserViewModel.kt @@ -18,22 +18,34 @@ package im.vector.riotredesign.features.reactions import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import im.vector.riotredesign.core.utils.LiveEvent class EmojiChooserViewModel : ViewModel() { var adapter: EmojiRecyclerAdapter? = null val emojiSourceLiveData: MutableLiveData = MutableLiveData() + val navigateEvent: MutableLiveData> = MutableLiveData() + var selectedReaction: String? = null + var eventId: String? = null + val currentSection: MutableLiveData = MutableLiveData() + var reactionClickListener = object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + selectedReaction = reaction + navigateEvent.value = LiveEvent(NAVIGATE_FINISH) + } + } + fun initWithContect(context: Context) { //TODO load async val emojiDataSource = EmojiDataSource(context) emojiSourceLiveData.value = emojiDataSource - adapter = EmojiRecyclerAdapter(emojiDataSource) + adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener) adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener { override fun firstVisibleSectionChange(section: Int) { - currentSection.value = section + currentSection.value = section } } @@ -42,4 +54,8 @@ class EmojiChooserViewModel : ViewModel() { fun scrollToSection(sectionIndex: Int) { adapter?.scrollToSection(sectionIndex) } + + companion object { + const val NAVIGATE_FINISH = "NAVIGATE_FINISH" + } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt index a6b27b0314..7c8d537b6c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiReactionPickerActivity.kt @@ -15,6 +15,7 @@ */ package im.vector.riotredesign.features.reactions +import android.app.Activity import android.content.Context import android.content.Intent import android.graphics.Typeface @@ -88,6 +89,8 @@ class EmojiReactionPickerActivity : VectorBaseActivity() { viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java) + viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID) + viewModel.emojiSourceLiveData.observe(this, Observer { it.rawData?.categories?.let { categories -> for (category in categories) { @@ -106,6 +109,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity() { tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) } }) + + viewModel.navigateEvent.observe(this, Observer { + it.getContentIfNotHandled()?.let { + if (it == EmojiChooserViewModel.NAVIGATE_FINISH) { + //finish with result + val dataResult = Intent() + dataResult.putExtra(EXTRA_REACTION_RESULT, viewModel.selectedReaction) + dataResult.putExtra(EXTRA_EVENT_ID, viewModel.eventId) + setResult(Activity.RESULT_OK, dataResult) + finish() + } + } + }) } private fun requestEmojivUnicode10CompatibleFont() { @@ -172,9 +188,13 @@ class EmojiReactionPickerActivity : VectorBaseActivity() { } companion object { - fun intent(context: Context): Intent { + + const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID" + const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT" + + fun intent(context: Context, eventId: String): Intent { val intent = Intent(context, EmojiReactionPickerActivity::class.java) -// intent.putExtra(EXTRA_MATRIX_ID, matrixID) + intent.putExtra(EXTRA_EVENT_ID, eventId) return intent } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiRecyclerAdapter.kt index a725f03fbf..e9f83f729c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/EmojiRecyclerAdapter.kt @@ -43,7 +43,7 @@ import kotlin.math.abs * TODO: Performances * TODO: Scroll to section - Find a way to snap section to the top */ -class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : +class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null, var reactionClickListener: ReactionClickListener?) : RecyclerView.Adapter() { var interactionListener: InteractionListener? = null @@ -64,6 +64,22 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : val toUpdateWhenNotBusy = ArrayList>() + val itemClickListener = View.OnClickListener { view -> + mRecyclerView?.getChildLayoutPosition(view)?.let { itemPosition -> + if (itemPosition != RecyclerView.NO_POSITION) { + val categories = dataSource?.rawData?.categories ?: return@OnClickListener + val sectionNumber = getSectionForAbsoluteIndex(itemPosition) + if (!isSection(itemPosition)) { + val sectionMojis = categories[sectionNumber].emojis + val sectionOffset = getSectionOffset(sectionNumber) + val emoji = sectionMojis[itemPosition - sectionOffset] + val item = dataSource.rawData!!.emojis.getValue(emoji).emojiString() + reactionClickListener?.onReactionSelected(item) + } + } + } + } + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { super.onAttachedToRecyclerView(recyclerView) @@ -113,6 +129,7 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) : beginTraceSession("MyAdapter.onCreateViewHolder") val inflater = LayoutInflater.from(parent.context) val itemView = inflater.inflate(viewType, parent, false) + itemView.setOnClickListener(itemClickListener) val viewHolder = when (viewType) { R.layout.grid_section_header -> SectionViewHolder(itemView) else -> EmojiViewHolder(itemView) diff --git a/vector/src/main/java/im/vector/riotredesign/features/reactions/ReactionClickListener.kt b/vector/src/main/java/im/vector/riotredesign/features/reactions/ReactionClickListener.kt new file mode 100644 index 0000000000..97fe90408e --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/reactions/ReactionClickListener.kt @@ -0,0 +1,5 @@ +package im.vector.riotredesign.features.reactions + +interface ReactionClickListener { + fun onReactionSelected(reaction: String) +} \ No newline at end of file