Show edited annotation in timeline + simple edit history

This commit is contained in:
Valere 2019-05-21 14:12:18 +02:00
parent a5a9fa3750
commit 6f103101b6
10 changed files with 118 additions and 6 deletions

View File

@ -1,3 +1,18 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room


import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
@ -7,7 +22,9 @@ import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm import io.realm.Realm



/**
* Fetches annotations (reactions, edits...) associated to a given eventEntity from the data layer.
*/
internal class EventRelationExtractor { internal class EventRelationExtractor {


fun extractFrom(event: EventEntity, realm: Realm = event.realm): EventAnnotationsSummary? { fun extractFrom(event: EventEntity, realm: Realm = event.realm): EventAnnotationsSummary? {

View File

@ -19,8 +19,11 @@
package im.vector.riotredesign.core.resources package im.vector.riotredesign.core.resources


import android.content.Context import android.content.Context
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import im.vector.riotredesign.features.themes.ThemeUtils


class ColorProvider(private val context: Context) { class ColorProvider(private val context: Context) {


@ -28,4 +31,16 @@ class ColorProvider(private val context: Context) {
return ContextCompat.getColor(context, colorRes) return ContextCompat.getColor(context, colorRes)
} }


/**
* Translates color attributes to colors
*
* @param c Context
* @param colorAttribute Color Attribute
* @return Requested Color
*/
@ColorInt
fun getColorFromAttribute(@AttrRes colorAttribute: Int): Int {
return ThemeUtils.getColor(context, colorAttribute)
}

} }

View File

@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home


import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandController import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandController
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserController import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserController
@ -58,7 +59,8 @@ class HomeModule {
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get()) val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
val timelineDateFormatter = TimelineDateFormatter(get()) val timelineDateFormatter = TimelineDateFormatter(get())
val timelineMediaSizeProvider = TimelineMediaSizeProvider() val timelineMediaSizeProvider = TimelineMediaSizeProvider()
val messageItemFactory = MessageItemFactory(get(), timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer) val colorProvider = ColorProvider(fragment.requireContext())
val messageItemFactory = MessageItemFactory(colorProvider, timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer)


val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory, val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory,
roomNameItemFactory = RoomNameItemFactory(get()), roomNameItemFactory = RoomNameItemFactory(get()),

View File

@ -17,6 +17,7 @@
package im.vector.riotredesign.features.home.room.detail package im.vector.riotredesign.features.home.room.detail


import com.jaiselrahman.filepicker.model.MediaFile import com.jaiselrahman.filepicker.model.MediaFile
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent


@ -31,6 +32,7 @@ sealed class RoomDetailActions {
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String,val selectedReaction: String,val opposite: String) : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String,val selectedReaction: String,val opposite: String) : RoomDetailActions()
data class ShowEditHistoryAction(val event: String,val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
object AcceptInvite : RoomDetailActions() object AcceptInvite : RoomDetailActions()
object RejectInvite : RoomDetailActions() object RejectInvite : RoomDetailActions()



View File

@ -53,6 +53,7 @@ import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -177,6 +178,12 @@ class RoomDetailFragment :
textComposerViewModel.subscribe { renderTextComposerState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }


roomDetailViewModel.nonBlockingPopAlert.observe(this, Observer { liveEvent ->
liveEvent.getContentIfNotHandled()?.let {
val message = requireContext().getString(it.first, *it.second.toTypedArray())
showSnackWithMessage(message, Snackbar.LENGTH_LONG)
}
})
actionViewModel.actionCommandEvent.observe(this, Observer { actionViewModel.actionCommandEvent.observe(this, Observer {
handleActions(it) handleActions(it)
}) })
@ -514,6 +521,12 @@ class RoomDetailFragment :
} }
} }


override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
editAggregatedSummary?.also {
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
}

}
// AutocompleteUserPresenter.Callback // AutocompleteUserPresenter.Callback


override fun onQueryUsers(query: CharSequence?) { override fun onQueryUsers(query: CharSequence?) {
@ -641,6 +654,12 @@ class RoomDetailFragment :
} }
} }


fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {
val snack = Snackbar.make(view!!, message, Snackbar.LENGTH_SHORT)
snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color))
snack.show()
}

// VectorInviteView.Callback // VectorInviteView.Callback


override fun onAcceptInvite() { override fun onAcceptInvite() {

View File

@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.VectorViewModel import im.vector.riotredesign.core.platform.VectorViewModel
import im.vector.riotredesign.core.utils.LiveEvent import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.command.CommandParser import im.vector.riotredesign.features.command.CommandParser
@ -36,6 +37,8 @@ import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


class RoomDetailViewModel(initialState: RoomDetailViewState, class RoomDetailViewModel(initialState: RoomDetailViewState,
@ -83,10 +86,16 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
is RoomDetailActions.RedactAction -> handleRedactEvent(action) is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action) is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
} }
} }




private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
get() = _nonBlockingPopAlert


private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>() private val _sendMessageResultLiveData = MutableLiveData<LiveEvent<SendMessageResult>>()
val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>> val sendMessageResultLiveData: LiveData<LiveEvent<SendMessageResult>>
get() = _sendMessageResultLiveData get() = _sendMessageResultLiveData
@ -161,6 +170,22 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
} }
} }


private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) {
//TODO temporary implementation
val lastReplace = action.editAggregatedSummary.sourceEvents.lastOrNull()?.let {
room.getTimeLineEvent(it)
} ?: return

val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
_nonBlockingPopAlert.postValue(LiveEvent(
Pair(R.string.last_edited_info_message, listOf(
lastReplace.senderName ?: "?",
dateFormat.format(Date(lastReplace.root.originServerTs ?: 0)))
))
)
}


private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))



View File

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
@ -58,6 +59,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
fun onAvatarClicked(informationData: MessageInformationData) fun onAvatarClicked(informationData: MessageInformationData)
fun onMemberNameClicked(informationData: MessageInformationData) fun onMemberNameClicked(informationData: MessageInformationData)
fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?)
} }


interface ReactionPillCallback { interface ReactionPillCallback {

View File

@ -51,7 +51,8 @@ class MessageActionsViewModel(initialState: MessageActionState) : VectorViewMode


val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
return if (event != null) { return if (event != null) {
val messageContent: MessageContent? = event.root.content.toModel() val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel()
val originTs = event.root.originServerTs val originTs = event.root.originServerTs
MessageActionState( MessageActionState(
event.root.sender ?: "", event.root.sender ?: "",

View File

@ -16,10 +16,12 @@


package im.vector.riotredesign.features.home.room.detail.timeline.factory package im.vector.riotredesign.features.home.room.detail.timeline.factory


import android.graphics.Color
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.TextPaint
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.view.View import android.view.View
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixLinkify
@ -27,6 +29,7 @@ import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
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.RelationType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -108,7 +111,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, hasBeenEdited, callback) is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, hasBeenEdited,
event.annotations?.editSummary,
callback
)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
@ -269,6 +275,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
private fun buildTextMessageItem(sendState: SendState, messageContent: MessageTextContent, private fun buildTextMessageItem(sendState: SendState, messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
hasBeenEdited: Boolean, hasBeenEdited: Boolean,
editSummary: EditAggregatedSummary?,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {


val bodyToUse = messageContent.formattedBody?.let { val bodyToUse = messageContent.formattedBody?.let {
@ -284,7 +291,28 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
spannable.append(linkifiedBody) spannable.append(linkifiedBody)
val editedSuffix = "(edited)" val editedSuffix = "(edited)"
spannable.append(" ").append(editedSuffix) spannable.append(" ").append(editedSuffix)
spannable.setSpan(ForegroundColorSpan(Color.LTGRAY), spannable.indexOf(editedSuffix), spannable.indexOf(editedSuffix) + editedSuffix.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) val color = colorProvider.getColorFromAttribute(R.attr.vctr_list_header_secondary_text_color)
val editStart = spannable.indexOf(editedSuffix)
val editEnd = editStart + editedSuffix.length
spannable.setSpan(
ForegroundColorSpan(color),
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)

spannable.setSpan(RelativeSizeSpan(.9f), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(object : ClickableSpan() {
override fun onClick(widget: View?) {
callback?.onEditedDecorationClicked(informationData, editSummary)
}

override fun updateDrawState(ds: TextPaint?) {
//nop
}
},
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
message(spannable) message(spannable)
} else { } else {
message(linkifiedBody) message(linkifiedBody)

View File

@ -15,5 +15,6 @@


<string name="event_redacted_by_user_reason">Event deleted by user</string> <string name="event_redacted_by_user_reason">Event deleted by user</string>
<string name="event_redacted_by_admin_reason">Event moderated by room admin</string> <string name="event_redacted_by_admin_reason">Event moderated by room admin</string>
<string name="last_edited_info_message">Last edited by %s on %s</string>


</resources> </resources>