mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-23 01:45:52 +03:00
Merge pull request #138 from vector-im/feature/send_reaction-phase1
Send reaction view quick react and picker
This commit is contained in:
commit
41c54029b5
20 changed files with 309 additions and 58 deletions
|
@ -49,4 +49,6 @@ interface SendService {
|
|||
fun sendMedias(attachments: List<ContentAttachmentData>): Cancelable
|
||||
|
||||
|
||||
fun sendReaction(reaction: String, targetEventId: String) : Cancelable
|
||||
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() })
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?>()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ?: "")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package im.vector.riotredesign.features.reactions
|
||||
|
||||
interface ReactionClickListener {
|
||||
fun onReactionSelected(reaction: String)
|
||||
}
|
Loading…
Reference in a new issue