Send reaction view quick react and picker

+ fix / Error when to many reactions in cells (more than placeholders -8-)
+ fix / DefaultTimeline quick map access was not shifted when items inserted at given index
This commit is contained in:
Valere 2019-05-16 16:35:54 +02:00
parent e27367e3f2
commit ef26519993
18 changed files with 266 additions and 57 deletions

View File

@ -49,4 +49,6 @@ interface SendService {
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable




fun sendReaction(reaction: String, targetEventId: String) : Cancelable

} }

View File

@ -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.Content
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.ReactionContent
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
@ -156,4 +157,20 @@ internal interface RoomAPI {
@Path("state_event_type") stateEventType: String, @Path("state_event_type") stateEventType: String,
@Path("state_key") stateKey: String, @Path("state_key") stateKey: String,
@Body params: Map<String, String>): Call<Unit> @Body params: Map<String, String>): Call<Unit>

/**
* 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<SendResponse>
} }

View File

@ -74,6 +74,18 @@ internal class DefaultSendService(private val roomId: String,
return cancelableBag 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 { override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
// Create an event with the media file path // Create an event with the media file path
val event = eventFactory.createMediaEvent(roomId, attachment).also { val event = eventFactory.createMediaEvent(roomId, attachment).also {
@ -116,6 +128,19 @@ internal class DefaultSendService(private val roomId: String,
.build() .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<SendEventWorker>()
.setConstraints(WORK_CONSTRAINTS)
.setInputData(sendWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}

private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadContentWorker.Params(roomId, event, attachment) val uploadMediaWorkerParams = UploadContentWorker.Params(roomId, event, attachment)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)

View File

@ -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.content.ContentAttachmentData
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.EventType 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.events.model.toContent
import im.vector.matrix.android.api.session.room.model.message.AudioInfo import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent
import im.vector.matrix.android.api.session.room.model.message.FileInfo import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo
import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.*
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.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.content.ThumbnailExtractor


internal class LocalEchoEventFactory(private val credentials: Credentials) { 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.IMAGE -> createImageEvent(roomId, attachment)
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment)
ContentAttachmentData.Type.AUDIO -> createAudioEvent(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 { private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event {
val content = MessageImageContent( val content = MessageImageContent(
type = MessageType.MSGTYPE_IMAGE, type = MessageType.MSGTYPE_IMAGE,

View File

@ -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<RoomAPI>()

override fun doWork(): Result {
val params = WorkerParamsFactory.fromData<SendRelationWorker.Params>(inputData)
?: return Result.failure()

val localEvent = params.event
if (localEvent.eventId == null) {
return Result.failure()
}
val relationContent = localEvent.content.toModel<ReactionContent>()
?: 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<SendResponse> {
apiCall = roomAPI.sendRelation(
roomId = params.roomId,
parent_id = relatedEventId,
relationType = relationType,
eventType = localEvent.type,
content = localEvent.content
)
}
return result.fold({ Result.retry() }, { Result.success() })
}
}

View File

@ -419,6 +419,8 @@ internal class DefaultTimeline(
val timelineEvent = timelineEventFactory.create(eventEntity) val timelineEvent = timelineEventFactory.create(eventEntity)
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
builtEvents.add(position, timelineEvent) builtEvents.add(position, timelineEvent)
//Need to shift :/
builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) }
builtEventsIdMap[eventEntity.eventId] = position builtEventsIdMap[eventEntity.eventId] = position
} }
Timber.v("Built ${offsetResults.size} items from db") Timber.v("Built ${offsetResults.size} items from db")

View File

@ -28,4 +28,8 @@ sealed class RoomDetailActions {
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()


data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()



} }

View File

@ -107,6 +107,7 @@ data class RoomDetailArgs(
private const val CAMERA_VALUE_TITLE = "attachment" private const val CAMERA_VALUE_TITLE = "attachment"
private const val REQUEST_FILES_REQUEST_CODE = 0 private const val REQUEST_FILES_REQUEST_CODE = 0
private const val TAKE_IMAGE_REQUEST_CODE = 1 private const val TAKE_IMAGE_REQUEST_CODE = 1
private const val REACTION_SELECT_REQUEST_CODE = 2


class RoomDetailFragment : class RoomDetailFragment :
VectorBaseFragment(), VectorBaseFragment(),
@ -182,6 +183,12 @@ 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 -> {
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))
}
} }
} }
} }
@ -487,7 +494,8 @@ class RoomDetailFragment :


when (actionData.actionId) { when (actionData.actionId) {
MessageMenuViewModel.ACTION_ADD_REACTION -> { 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 -> { MessageMenuViewModel.ACTION_COPY -> {
//I need info about the current selected message :/ //I need info about the current selected message :/
@ -539,6 +547,11 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() }
.show() .show()
} }
MessageMenuViewModel.ACTION_QUICK_REACT -> {
(actionData.data as? Pair<String, String>)?.let { pairData ->
roomDetailViewModel.process(RoomDetailActions.SendReaction(pairData.second, pairData.first))
}
}
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

@ -69,11 +69,12 @@ 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)
} }
} }


@ -88,63 +89,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))
} }
@ -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) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachments = action.mediaFiles.map { val attachments = action.mediaFiles.map {
ContentAttachmentData( ContentAttachmentData(

View File

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

View File

@ -67,7 +67,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes


//TODO determine if can copy, forward, reply, quote, report? //TODO determine if can copy, forward, reply, quote, report?
val actions = ArrayList<SimpleAction>().apply { val actions = ArrayList<SimpleAction>().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)) { if (canCopy(type)) {
//TODO copy images? html? see ClipBoard //TODO copy images? html? see ClipBoard
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body)) this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent.body))
@ -184,6 +184,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE" const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
const val PERMALINK = "PERMALINK" const val PERMALINK = "PERMALINK"
const val ACTION_FLAG = "ACTION_FLAG" const val ACTION_FLAG = "ACTION_FLAG"
const val ACTION_QUICK_REACT = "ACTION_QUICK_REACT"




} }

View File

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


if (it.selectionResult != null) { if (it.selectionResult != null) {
interactionListener?.didQuickReactWith(it.selectionResult) interactionListener?.didQuickReactWith(it.selectionResult.first, it.selectionResult.second, it.eventId)
} }
} }


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


companion object { companion object {

View File

@ -31,7 +31,12 @@ enum class TriggleState {
SECOND SECOND
} }


data class QuickReactionState(val agreeTrigleState: TriggleState, val likeTriggleState: TriggleState, val selectionResult: List<String>? = null) : MvRxState data class QuickReactionState(
val agreeTrigleState: TriggleState,
val likeTriggleState: TriggleState,
/** Pair of 'clickedOn' and current toggles state*/
val selectionResult: Pair<String, List<String>>? = null,
val eventId: String) : MvRxState


/** /**
* Quick reaction view model * Quick reaction view model
@ -43,16 +48,18 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
fun toggleAgree(isFirst: Boolean) = withState { fun toggleAgree(isFirst: Boolean) = withState {
if (isFirst) { if (isFirst) {
setState { setState {
val newTriggle = if (it.agreeTrigleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
copy( copy(
agreeTrigleState = if (it.agreeTrigleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST, agreeTrigleState = newTriggle,
selectionResult = getReactions(this) selectionResult = Pair(agreePositive, getReactions(this, newTriggle, null))
) )
} }
} else { } else {
setState { setState {
val newTriggle = if (it.agreeTrigleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
copy( copy(
agreeTrigleState = if (it.agreeTrigleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND, agreeTrigleState = agreeTrigleState,
selectionResult = getReactions(this) selectionResult = Pair(agreeNegative, getReactions(this, newTriggle, null))
) )
} }
} }
@ -61,30 +68,32 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
fun toggleLike(isFirst: Boolean) = withState { fun toggleLike(isFirst: Boolean) = withState {
if (isFirst) { if (isFirst) {
setState { setState {
val newTriggle = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST
copy( copy(
likeTriggleState = if (it.likeTriggleState == TriggleState.FIRST) TriggleState.NONE else TriggleState.FIRST, likeTriggleState = newTriggle,
selectionResult = getReactions(this) selectionResult = Pair(likePositive, getReactions(this, null, newTriggle))
) )
} }
} else { } else {
setState { setState {
val newTriggle = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND
copy( copy(
likeTriggleState = if (it.likeTriggleState == TriggleState.SECOND) TriggleState.NONE else TriggleState.SECOND, likeTriggleState = newTriggle,
selectionResult = getReactions(this) selectionResult = Pair(likeNegative, getReactions(this, null, newTriggle))
) )
} }
} }
} }


private fun getReactions(state: QuickReactionState): List<String> { private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List<String> {
return ArrayList<String>(4).apply { return ArrayList<String>(4).apply {
when (state.likeTriggleState) { when (newState2 ?: state.likeTriggleState) {
TriggleState.FIRST -> add(likePositive) TriggleState.FIRST -> add(likePositive)
TriggleState.SECOND -> add(likeNegative) TriggleState.SECOND -> add(likeNegative)
else -> { else -> {
} }
} }
when (state.agreeTrigleState) { when (newState1 ?: state.agreeTrigleState) {
TriggleState.FIRST -> add(agreePositive) TriggleState.FIRST -> add(agreePositive)
TriggleState.SECOND -> add(agreeNegative) TriggleState.SECOND -> add(agreeNegative)
else -> { else -> {
@ -126,7 +135,7 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel
} }
} }
} }
return QuickReactionState(agreeTriggle, likeTriggle) return QuickReactionState(agreeTriggle, likeTriggle, null, event.root.eventId ?: "")
} }
} }
} }

View File

@ -88,8 +88,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
//clear all reaction buttons (but not the Flow helper!) //clear all reaction buttons (but not the Flow helper!)
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
val idToRefInFlow = ArrayList<Int>() val idToRefInFlow = ArrayList<Int>()
informationData.orderedReactionList?.forEachIndexed { index, reaction -> informationData.orderedReactionList?.chunked(7)?.firstOrNull()?.forEachIndexed { index, reaction ->
(holder.reactionWrapper?.children?.elementAt(index) as? ReactionButton)?.let { reactionButton -> (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
reactionButton.isVisible = true reactionButton.isVisible = true
idToRefInFlow.add(reactionButton.id) idToRefInFlow.add(reactionButton.id)
reactionButton.reactionString = reaction.first reactionButton.reactionString = reaction.first

View File

@ -18,22 +18,34 @@ package im.vector.riotredesign.features.reactions
import android.content.Context import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import im.vector.riotredesign.core.utils.LiveEvent


class EmojiChooserViewModel : ViewModel() { class EmojiChooserViewModel : ViewModel() {


var adapter: EmojiRecyclerAdapter? = null var adapter: EmojiRecyclerAdapter? = null
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData() val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()


val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var selectedReaction: String? = null
var eventId: String? = null

val currentSection: MutableLiveData<Int> = MutableLiveData() val currentSection: MutableLiveData<Int> = MutableLiveData()


var reactionClickListener = object : ReactionClickListener {
override fun onReactionSelected(reaction: String) {
selectedReaction = reaction
navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
}
}

fun initWithContect(context: Context) { fun initWithContect(context: Context) {
//TODO load async //TODO load async
val emojiDataSource = EmojiDataSource(context) val emojiDataSource = EmojiDataSource(context)
emojiSourceLiveData.value = emojiDataSource emojiSourceLiveData.value = emojiDataSource
adapter = EmojiRecyclerAdapter(emojiDataSource) adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener)
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener { adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
override fun firstVisibleSectionChange(section: Int) { override fun firstVisibleSectionChange(section: Int) {
currentSection.value = section currentSection.value = section
} }


} }
@ -42,4 +54,8 @@ class EmojiChooserViewModel : ViewModel() {
fun scrollToSection(sectionIndex: Int) { fun scrollToSection(sectionIndex: Int) {
adapter?.scrollToSection(sectionIndex) adapter?.scrollToSection(sectionIndex)
} }

companion object {
const val NAVIGATE_FINISH = "NAVIGATE_FINISH"
}
} }

View File

@ -15,6 +15,7 @@
*/ */
package im.vector.riotredesign.features.reactions package im.vector.riotredesign.features.reactions


import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
@ -88,6 +89,8 @@ class EmojiReactionPickerActivity : VectorBaseActivity() {


viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java) viewModel = ViewModelProviders.of(this).get(EmojiChooserViewModel::class.java)


viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID)

viewModel.emojiSourceLiveData.observe(this, Observer { viewModel.emojiSourceLiveData.observe(this, Observer {
it.rawData?.categories?.let { categories -> it.rawData?.categories?.let { categories ->
for (category in categories) { for (category in categories) {
@ -106,6 +109,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity() {
tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) 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() { private fun requestEmojivUnicode10CompatibleFont() {
@ -172,9 +188,13 @@ class EmojiReactionPickerActivity : VectorBaseActivity() {
} }


companion object { 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) val intent = Intent(context, EmojiReactionPickerActivity::class.java)
// intent.putExtra(EXTRA_MATRIX_ID, matrixID) intent.putExtra(EXTRA_EVENT_ID, eventId)
return intent return intent
} }
} }

View File

@ -43,7 +43,7 @@ import kotlin.math.abs
* TODO: Performances * TODO: Performances
* TODO: Scroll to section - Find a way to snap section to the top * 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<EmojiRecyclerAdapter.ViewHolder>() { RecyclerView.Adapter<EmojiRecyclerAdapter.ViewHolder>() {


var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
@ -64,6 +64,22 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :


val toUpdateWhenNotBusy = ArrayList<Pair<String, EmojiViewHolder>>() val toUpdateWhenNotBusy = ArrayList<Pair<String, EmojiViewHolder>>()


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) { override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView) super.onAttachedToRecyclerView(recyclerView)
@ -113,6 +129,7 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
beginTraceSession("MyAdapter.onCreateViewHolder") beginTraceSession("MyAdapter.onCreateViewHolder")
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(viewType, parent, false) val itemView = inflater.inflate(viewType, parent, false)
itemView.setOnClickListener(itemClickListener)
val viewHolder = when (viewType) { val viewHolder = when (viewType) {
R.layout.grid_section_header -> SectionViewHolder(itemView) R.layout.grid_section_header -> SectionViewHolder(itemView)
else -> EmojiViewHolder(itemView) else -> EmojiViewHolder(itemView)

View File

@ -0,0 +1,5 @@
package im.vector.riotredesign.features.reactions

interface ReactionClickListener {
fun onReactionSelected(reaction: String)
}