Fix / Bug aggregation on initial sync

fix / All messages were not processed due to a test exiting the for loop
+ started adding context menu for non room messages
This commit is contained in:
Valere 2019-06-07 10:01:42 +02:00
parent 3f1bf00fdd
commit 7409003949
15 changed files with 233 additions and 134 deletions

View File

@ -48,7 +48,7 @@ android {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"


// Set to BODY instead of NONE to enable logging // Set to BODY instead of NONE to enable logging
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC" buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
} }


release { release {
@ -91,7 +91,7 @@ dependencies {
def moshi_version = '1.8.0' def moshi_version = '1.8.0'
def lifecycle_version = '2.0.0' def lifecycle_version = '2.0.0'
def coroutines_version = "1.0.1" def coroutines_version = "1.0.1"
def markwon_version = '3.0.0-SNAPSHOT' def markwon_version = '3.0.0'


implementation fileTree(dir: 'libs', include: ['*.aar']) implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.database.mapper package im.vector.matrix.android.internal.database.mapper


import com.squareup.moshi.JsonDataException
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.UnsignedData import im.vector.matrix.android.api.session.events.model.UnsignedData
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
@ -46,8 +47,16 @@ internal object EventMapper {


fun map(eventEntity: EventEntity): Event { fun map(eventEntity: EventEntity): Event {
//TODO proxy the event to only parse unsigned data when accessed? //TODO proxy the event to only parse unsigned data when accessed?
var ud = if (eventEntity.unsignedData.isNullOrBlank()) null val ud = if (eventEntity.unsignedData.isNullOrBlank()) {
else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData) null
} else {
try {
MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData)
} catch (t: JsonDataException) {
null
}

}
return Event( return Event(
type = eventEntity.type, type = eventEntity.type,
eventId = eventEntity.eventId, eventId = eventEntity.eventId,

View File

@ -27,7 +27,6 @@ import im.vector.matrix.android.internal.database.model.*
import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.create
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionAsync
import im.vector.matrix.android.internal.util.tryTransactionSync import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm import io.realm.Realm
import timber.log.Timber import timber.log.Timber
@ -49,60 +48,75 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
private val SHOULD_HANDLE_SERVER_AGREGGATION = false private val SHOULD_HANDLE_SERVER_AGREGGATION = false


override fun execute(params: EventRelationsAggregationTask.Params): Try<Unit> { override fun execute(params: EventRelationsAggregationTask.Params): Try<Unit> {
val events = params.events
val userId = params.userId
return monarchy.tryTransactionSync { realm -> return monarchy.tryTransactionSync { realm ->
update(realm, params.events, params.userId) Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events")
update(realm, events, userId)
Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished")
} }
} }


private fun update(realm: Realm, events: List<Pair<Event, SendState>>, userId: String) { private fun update(realm: Realm, events: List<Pair<Event, SendState>>, userId: String) {
events.forEach { pair -> events.forEach { pair ->
val roomId = pair.first.roomId ?: return@forEach try { //Temporary catch, should be removed
val event = pair.first val roomId = pair.first.roomId
val sendState = pair.second if (roomId == null) {
val isLocalEcho = sendState == SendState.UNSENT Timber.w("Event has no room id ${pair.first.eventId}")
when (event.type) { return@forEach
EventType.REACTION -> {
//we got a reaction!!
Timber.v("###REACTION in room $roomId")
handleReaction(event, roomId, realm, userId, isLocalEcho)
} }
EventType.MESSAGE -> { val event = pair.first
if (event.unsignedData?.relations?.annotations != null) { val sendState = pair.second
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") val isLocalEcho = sendState == SendState.UNSENT
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) when (event.type) {
} else { EventType.REACTION -> {
val content: MessageContent? = event.content.toModel() //we got a reaction!!
if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") handleReaction(event, roomId, realm, userId, isLocalEcho)
//A replace!
handleReplace(realm, event, content, roomId, isLocalEcho)
}
} }

EventType.MESSAGE -> {
} if (event.unsignedData?.relations?.annotations != null) {
EventType.REDACTION -> { Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
?: return } else {
when (eventToPrune.type) { val content: MessageContent? = event.content.toModel()
EventType.MESSAGE -> { if (content?.relatesTo?.type == RelationType.REPLACE) {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData //A replace!
?: UnsignedData(null, null) handleReplace(realm, event, content, roomId, isLocalEcho)

//was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
} }

} }
EventType.REACTION -> {
handleReactionRedact(eventToPrune, realm, userId) }
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: return@forEach
when (eventToPrune.type) {
EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData
?: UnsignedData(null, null)

//was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
}

}
EventType.REACTION -> {
handleReactionRedact(eventToPrune, realm, userId)
}
} }
} }
else -> Timber.v("UnHandled event ${event.eventId}")
} }

} catch (t: Throwable) {
Timber.e(t, "## Should not happen ")
} }
} }

} }


private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) { private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) {
@ -112,7 +126,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
//ok, this is a replace //ok, this is a replace
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
if (existing == null) { if (existing == null) {
Timber.v("###REPLACE creating no relation summary for ${targetEventId}") Timber.v("###REPLACE creating new relation summary for ${targetEventId}")
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) existing = EventAnnotationsSummaryEntity.create(realm, targetEventId)
existing.roomId = roomId existing.roomId = roomId
} }
@ -120,7 +134,7 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
//we have it //we have it
val existingSummary = existing.editSummary val existingSummary = existing.editSummary
if (existingSummary == null) { if (existingSummary == null) {
Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)") Timber.v("###REPLACE new edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)")
//create the edit summary //create the edit summary
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
@ -181,62 +195,70 @@ internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarc
} }


private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
event.content.toModel<ReactionContent>()?.let { content -> val content = event.content.toModel<ReactionContent>()
//rel_type must be m.annotation if (content == null) {
if (RelationType.ANNOTATION == content.relatesTo?.type) { Timber.e("Malformed reaction content ${event.content}")
val reaction = content.relatesTo.key return
val eventId = content.relatesTo.eventId }
val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() //rel_type must be m.annotation
?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } if (RelationType.ANNOTATION == content.relatesTo?.type) {
val reaction = content.relatesTo.key
val relatedEventID = content.relatesTo.eventId
val reactionEventId = event.eventId
Timber.v("Reaction $reactionEventId relates to $relatedEventID")
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventID).findFirst()
?: EventAnnotationsSummaryEntity.create(realm, relatedEventID).apply { this.roomId = roomId }


var sum = eventSummary.reactionsSummary.find { it.key == reaction } var sum = eventSummary.reactionsSummary.find { it.key == reaction }
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
if (isLocalEcho && txId.isNullOrBlank()) { if (isLocalEcho && txId.isNullOrBlank()) {
Timber.w("Received a local echo with no transaction ID") Timber.w("Received a local echo with no transaction ID")
} }
if (sum == null) { if (sum == null) {
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.key = reaction sum.key = reaction
sum.firstTimestamp = event.originServerTs ?: 0 sum.firstTimestamp = event.originServerTs ?: 0
if (isLocalEcho) { if (isLocalEcho) {
Timber.v("Adding local echo reaction $reaction") Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId) sum.sourceLocalEcho.add(txId)
sum.count = 1 sum.count = 1
} else {
Timber.v("Adding synced reaction $reaction")
sum.count = 1
sum.sourceEvents.add(event.eventId)
}
sum.addedByMe = sum.addedByMe || (userId == event.sender)
eventSummary.reactionsSummary.add(sum)
} else { } else {
//is this a known event (is possible? pagination?) Timber.v("Adding synced reaction $reaction")
if (!sum.sourceEvents.contains(eventId)) { sum.count = 1
sum.sourceEvents.add(reactionEventId)
}
sum.addedByMe = sum.addedByMe || (userId == event.sender)
eventSummary.reactionsSummary.add(sum)
} else {
//is this a known event (is possible? pagination?)
if (!sum.sourceEvents.contains(reactionEventId)) {


//check if it's not the sync of a local echo //check if it's not the sync of a local echo
if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) {
//ok it has already been counted, just sync the list, do not touch count //ok it has already been counted, just sync the list, do not touch count
Timber.v("Ignoring synced of local echo for reaction $reaction") Timber.v("Ignoring synced of local echo for reaction $reaction")
sum.sourceLocalEcho.remove(txId) sum.sourceLocalEcho.remove(txId)
sum.sourceEvents.add(event.eventId) sum.sourceEvents.add(reactionEventId)
} else {
sum.count += 1
if (isLocalEcho) {
Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId)
} else { } else {
sum.count += 1 Timber.v("Adding synced reaction $reaction")
if (isLocalEcho) { sum.sourceEvents.add(reactionEventId)
Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId)
} else {
Timber.v("Adding synced reaction $reaction")
sum.sourceEvents.add(event.eventId)
}

sum.addedByMe = sum.addedByMe || (userId == event.sender)
} }


sum.addedByMe = sum.addedByMe || (userId == event.sender)
} }
}


}
} }

} else {
Timber.e("Unknwon relation type ${content.relatesTo?.type} for event ${event.eventId}")
} }

} }


/** /**

View File

@ -46,11 +46,11 @@ internal class EventRelationsAggregationUpdater(monarchy: Monarchy,


override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) { override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions") Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions")
val inserted = inserted val domainInserted = inserted
.mapNotNull { it.asDomain() to it.sendState } .map { it.asDomain() to it.sendState }


val params = EventRelationsAggregationTask.Params( val params = EventRelationsAggregationTask.Params(
inserted, domainInserted,
credentials.userId credentials.userId
) )



View File

@ -38,7 +38,12 @@ internal class TaskExecutor(private val coroutineDispatchers: MatrixCoroutineDis
task.execute(task.params) task.execute(task.params)
} }
} }
resultOrFailure.fold({ task.callback.onFailure(it) }, { task.callback.onSuccess(it) }) resultOrFailure.fold({
Timber.d(it, "Task failed")
task.callback.onFailure(it)
}, {
task.callback.onSuccess(it)
})
} }
return CancelableCoroutine(job) return CancelableCoroutine(job)
} }

View File

@ -84,13 +84,15 @@ android {
debug { debug {
resValue "bool", "debug_mode", "true" resValue "bool", "debug_mode", "true"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"

buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }


release { release {
resValue "bool", "debug_mode", "false" resValue "bool", "debug_mode", "false"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
buildConfigField "boolean", "SHOW_HIDDEN_TIMELINE_EVENTS", "false"


minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
@ -132,7 +134,7 @@ dependencies {
def epoxy_version = "3.3.0" def epoxy_version = "3.3.0"
def arrow_version = "0.8.2" def arrow_version = "0.8.2"
def coroutines_version = "1.0.1" def coroutines_version = "1.0.1"
def markwon_version = '3.0.0-SNAPSHOT' def markwon_version = '3.0.0'
def big_image_viewer_version = '1.5.6' def big_image_viewer_version = '1.5.6'
def glide_version = '4.9.0' def glide_version = '4.9.0'
def moshi_version = '1.8.0' def moshi_version = '1.8.0'

View File

@ -563,11 +563,11 @@ class RoomDetailFragment :
vectorBaseActivity.notImplemented() vectorBaseActivity.notImplemented()
} }


override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View) { override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {


} }


override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean { override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = roomDetailArgs.roomId val roomId = roomDetailArgs.roomId



View File

@ -55,7 +55,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
private val roomId = initialState.roomId private val roomId = initialState.roomId
private val eventId = initialState.eventId private val eventId = initialState.eventId
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>() private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) private val timeline = room.createTimeline(eventId,
if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES else TimelineDisplayableEvents.DISPLAYABLE_TYPES)


companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> { companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {



View File

@ -46,15 +46,13 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler() private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {


interface Callback : ReactionPillCallback, AvatarCallback { interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback {
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onUrlClicked(url: String) fun onUrlClicked(url: String)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
fun onFileMessageClicked(messageFileContent: MessageFileContent) fun onFileMessageClicked(messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View)
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?)
} }


@ -63,6 +61,11 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String)
} }


interface BaseCallback {
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View)
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean
}

interface AvatarCallback { interface AvatarCallback {
fun onAvatarClicked(informationData: MessageInformationData) fun onAvatarClicked(informationData: MessageInformationData)
fun onMemberNameClicked(informationData: MessageInformationData) fun onMemberNameClicked(informationData: MessageInformationData)

View File

@ -34,7 +34,7 @@ import org.koin.android.ext.android.get


data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, val data: Any? = null) data class SimpleAction(val uid: String, val titleRes: Int, val iconResId: Int?, val data: Any? = null)


data class MessageMenuState(val actions: List<SimpleAction>) : MvRxState data class MessageMenuState(val actions: List<SimpleAction> = emptyList()) : MvRxState


/** /**
* Manages list actions for a given message (copy / paste / forward...) * Manages list actions for a given message (copy / paste / forward...)
@ -50,9 +50,9 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId) val event = currentSession.getRoom(parcel.roomId)?.getTimeLineEvent(parcel.eventId)
?: return null ?: return null


val messageContent: MessageContent = event.annotations?.editSummary?.aggregatedContent?.toModel() val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel() ?: return null ?: event.root.content.toModel()
val type = messageContent.type val type = messageContent?.type


if (event.sendState == SendState.UNSENT) { if (event.sendState == SendState.UNSENT) {
//Resend and Delete //Resend and Delete
@ -76,7 +76,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId)) this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, 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))
} }


if (canReply(event, messageContent)) { if (canReply(event, messageContent)) {
@ -134,10 +134,10 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
return MessageMenuState(actions) return MessageMenuState(actions)
} }


private fun canReply(event: TimelineEvent, messageContent: MessageContent): Boolean { private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.type != EventType.MESSAGE) return false if (event.root.type != EventType.MESSAGE) return false
return when (messageContent.type) { return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
@ -149,10 +149,10 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }
} }


private fun canQuote(event: TimelineEvent, messageContent: MessageContent): Boolean { private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.type != EventType.MESSAGE) return false if (event.root.type != EventType.MESSAGE) return false
return when (messageContent.type) { return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
@ -190,7 +190,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }




private fun canCopy(type: String): Boolean { private fun canCopy(type: String?): Boolean {
return when (type) { return when (type) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_NOTICE,
@ -204,7 +204,7 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
} }




private fun canShare(type: String): Boolean { private fun canShare(type: String?): Boolean {
return when (type) { return when (type) {
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,

View File

@ -17,23 +17,32 @@
package im.vector.riotredesign.features.home.room.detail.timeline.factory package im.vector.riotredesign.features.home.room.detail.timeline.factory


import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName import im.vector.riotredesign.features.home.room.detail.timeline.helper.senderName
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_


class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) { class NoticeItemFactory(private val eventFormatter: NoticeEventFormatter) {


fun create(event: TimelineEvent): NoticeItem? { fun create(event: TimelineEvent,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null val formattedText = eventFormatter.format(event) ?: return null
val senderName = event.senderName() val informationData = MessageInformationData(
val senderAvatar = event.senderAvatar() eventId = event.root.eventId ?: "?",
senderId = event.root.sender ?: "",
sendState = event.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
showInformation = false
)


return NoticeItem_() return NoticeItem_()
.noticeText(formattedText) .noticeText(formattedText)
.avatarUrl(senderAvatar) .informationData(informationData)
.memberName(senderName) .baseCallback(callback)
} }





View File

@ -17,10 +17,16 @@
package im.vector.riotredesign.features.home.room.detail.timeline.factory package im.vector.riotredesign.features.home.room.detail.timeline.factory


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.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageDefaultContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.EmptyItem_ import im.vector.riotredesign.core.epoxy.EmptyItem_
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
import timber.log.Timber import timber.log.Timber


class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
@ -43,7 +49,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
EventType.STATE_HISTORY_VISIBILITY, EventType.STATE_HISTORY_VISIBILITY,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> noticeItemFactory.create(event) EventType.CALL_ANSWER -> noticeItemFactory.create(event, callback)


// Unhandled event types (yet) // Unhandled event types (yet)
EventType.ENCRYPTED, EventType.ENCRYPTED,
@ -51,9 +57,32 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER, EventType.STICKER,
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)

else -> { else -> {
Timber.w("Ignored event (type: ${event.root.type}") //These are just for debug to display hidden event, they should be filtered out in normal mode
null if (TimelineDisplayableEvents.DEBUG_HIDDEN_EVENT) {
val informationData = MessageInformationData(eventId = event.root.eventId
?: "?",
senderId = event.root.sender ?: "",
sendState = event.sendState,
time = "",
avatarUrl = null,
memberName = "",
showInformation = false
)
val messageContent = event.root.content.toModel<MessageContent>()
?: MessageDefaultContent("", "", null, null)
MessageTextItem_()
.informationData(informationData)
.message("{ \"type\": ${event.root.type} }")
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
}
} else {
Timber.w("Ignored event (type: ${event.root.type}")
null
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -22,10 +22,14 @@ import im.vector.matrix.android.api.session.events.model.toModel
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.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime


object TimelineDisplayableEvents { object TimelineDisplayableEvents {


//Debug helper, to show invisible items in time line (reaction, redacts)
val DEBUG_HIDDEN_EVENT = BuildConfig.SHOW_HIDDEN_TIMELINE_EVENTS

val DISPLAYABLE_TYPES = listOf( val DISPLAYABLE_TYPES = listOf(
EventType.MESSAGE, EventType.MESSAGE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
@ -41,6 +45,11 @@ object TimelineDisplayableEvents {
EventType.STICKER, EventType.STICKER,
EventType.STATE_ROOM_CREATE EventType.STATE_ROOM_CREATE
) )

val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(
EventType.REDACTION,
EventType.REACTION
)
} }


fun TimelineEvent.isDisplayable(): Boolean { fun TimelineEvent.isDisplayable(): Boolean {

View File

@ -23,27 +23,37 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController


@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() { abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {


@EpoxyAttribute @EpoxyAttribute
var noticeText: CharSequence? = null var noticeText: CharSequence? = null
@EpoxyAttribute
var avatarUrl: String? = null
@EpoxyAttribute
var userId: String = ""
@EpoxyAttribute
var memberName: CharSequence? = null



@EpoxyAttribute @EpoxyAttribute
var longClickListener: View.OnLongClickListener? = null lateinit var informationData: MessageInformationData

@EpoxyAttribute
var baseCallback: TimelineEventController.BaseCallback? = null


private var longClickListener = View.OnLongClickListener {
baseCallback?.onEventLongClicked(informationData, null, it)
baseCallback != null
}



override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.noticeTextView.text = noticeText holder.noticeTextView.text = noticeText
AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView) AvatarRenderer.render(
informationData.avatarUrl,
informationData.senderId,
informationData.memberName?.toString()
?: informationData.senderId,
holder.avatarImageView
)
holder.view.setOnLongClickListener(longClickListener) holder.view.setOnLongClickListener(longClickListener)
} }


@ -51,7 +61,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {


class Holder : BaseHolder() { class Holder : BaseHolder() {
override fun getStubId(): Int = STUB_ID override fun getStubId(): Int = STUB_ID

val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView) val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView) val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
} }

View File

@ -4,6 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:addStatesFromChildren="true" android:addStatesFromChildren="true"
android:background="?attr/selectableItemBackground"
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingRight="8dp"> android:paddingRight="8dp">


@ -31,9 +32,9 @@
<ViewStub <ViewStub
android:id="@+id/messageContentBlankStub" android:id="@+id/messageContentBlankStub"
style="@style/TimelineContentStubNoInfoLayoutParams" style="@style/TimelineContentStubNoInfoLayoutParams"
android:layout="@layout/item_timeline_event_blank_stub"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout="@layout/item_timeline_event_blank_stub"
tools:ignore="MissingConstraints" /> tools:ignore="MissingConstraints" />


<ViewStub <ViewStub