Merge pull request #138 from vector-im/feature/send_reaction-phase1

Send reaction view quick react and picker
This commit is contained in:
Valere 2019-05-17 09:05:58 +02:00 committed by GitHub
commit 41c54029b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 309 additions and 58 deletions

View file

@ -49,4 +49,6 @@ interface SendService {
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.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<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
}
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<SendEventWorker>()
.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)

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.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,

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 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")

View file

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

View file

@ -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<String, String>)?.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

View file

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

View file

@ -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<String>()
private val mergeItemCollapseStates = HashMap<String, Boolean>()
private val modelCache = arrayListOf<CacheItemData?>()

View file

@ -95,8 +95,13 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() {
.commit()
}
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
override fun didQuickReactWith(reactions: List<String>) {
actionHandlerModel.fireAction("Quick React", reactions)
override fun didQuickReactWith(clikedOn: String, reactions: List<String>, 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()
}
}

View file

@ -67,7 +67,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
//TODO determine if can copy, forward, reply, quote, report?
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)) {
//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<Mes
const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
const val PERMALINK = "PERMALINK"
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) {
interactionListener?.didQuickReactWith(it.selectionResult)
interactionListener?.didQuickReactWith(it.selectionResult.first, it.selectionResult.second, it.eventId)
}
}
interface InteractionListener {
fun didQuickReactWith(reactions: List<String>)
fun didQuickReactWith(clikedOn: String, reactions: List<String>, eventId: String)
}
companion object {

View file

@ -31,7 +31,12 @@ enum class TriggleState {
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
@ -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<String> {
private fun getReactions(state: QuickReactionState, newState1: TriggleState?, newState2: TriggleState?): List<String> {
return ArrayList<String>(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 ?: "")
}
}
}

View file

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

View file

@ -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<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
@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<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
//clear all reaction buttons (but not the Flow helper!)
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
val idToRefInFlow = ArrayList<Int>()
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<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
}
}
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

View file

@ -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<EmojiDataSource> = MutableLiveData()
val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var selectedReaction: String? = null
var eventId: String? = null
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) {
//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"
}
}

View file

@ -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
}
}

View file

@ -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<EmojiRecyclerAdapter.ViewHolder>() {
var interactionListener: InteractionListener? = null
@ -64,6 +64,22 @@ class EmojiRecyclerAdapter(val dataSource: EmojiDataSource? = null) :
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) {
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)

View file

@ -0,0 +1,5 @@
package im.vector.riotredesign.features.reactions
interface ReactionClickListener {
fun onReactionSelected(reaction: String)
}