forked from GitHub-Mirror/riotX-android
Merge pull request #141 from vector-im/feature/edit_aggregation
Support incoming message edition
This commit is contained in:
commit
ec53ce9d00
@ -21,9 +21,10 @@ import im.vector.matrix.android.InstrumentedTest
|
|||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
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
|
||||||
|
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor
|
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||||
@ -49,7 +50,9 @@ internal class TimelineTest : InstrumentedTest {
|
|||||||
fun setup() {
|
fun setup() {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
Realm.init(context())
|
Realm.init(context())
|
||||||
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
|
val testConfiguration = RealmConfiguration.Builder().name("test-realm")
|
||||||
|
.modules(SessionRealmModule()).build()
|
||||||
|
|
||||||
Realm.deleteRealm(testConfiguration)
|
Realm.deleteRealm(testConfiguration)
|
||||||
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
|
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
|
||||||
RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
|
RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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.api.session.room.model
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
|
||||||
|
data class EditAggregatedSummary(
|
||||||
|
val aggregatedContent: Content? = null,
|
||||||
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
|
val sourceEvents: List<String>,
|
||||||
|
val lastEditTs: Long = 0
|
||||||
|
)
|
@ -1,7 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* 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.api.session.room.model
|
package im.vector.matrix.android.api.session.room.model
|
||||||
|
|
||||||
|
|
||||||
data class EventAnnotationsSummary(
|
data class EventAnnotationsSummary(
|
||||||
var eventId: String,
|
var eventId: String,
|
||||||
var reactionsSummary: List<ReactionAggregatedSummary>
|
var reactionsSummary: List<ReactionAggregatedSummary>,
|
||||||
|
var editSummary: EditAggregatedSummary?
|
||||||
)
|
)
|
@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ReactionInfo(
|
data class ReactionInfo(
|
||||||
@Json(name = "rel_type") override val type: String,
|
@Json(name = "rel_type") override val type: String?,
|
||||||
@Json(name = "event_id") override val eventId: String,
|
@Json(name = "event_id") override val eventId: String,
|
||||||
val key: String
|
val key: String
|
||||||
) : RelationContent
|
) : RelationContent
|
@ -1,6 +1,6 @@
|
|||||||
package im.vector.matrix.android.api.session.room.model.annotation
|
package im.vector.matrix.android.api.session.room.model.annotation
|
||||||
|
|
||||||
interface RelationContent {
|
interface RelationContent {
|
||||||
val type: String
|
val type: String?
|
||||||
val eventId: String
|
val eventId: String?
|
||||||
}
|
}
|
@ -1,8 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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.api.session.room.model.annotation
|
package im.vector.matrix.android.api.session.room.model.annotation
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
data class RelationDefaultContent(
|
data class RelationDefaultContent(
|
||||||
@Json(name = "rel_type") override val type: String,
|
@Json(name = "rel_type") override val type: String?,
|
||||||
@Json(name = "event_id") override val eventId: String
|
@Json(name = "event_id") override val eventId: String?
|
||||||
) : RelationContent
|
) : RelationContent
|
||||||
|
@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class FileInfo(
|
data class FileInfo(
|
||||||
@Json(name = "mimetype") val mimeType: String,
|
@Json(name = "mimetype") val mimeType: String?,
|
||||||
@Json(name = "size") val size: Long = 0,
|
@Json(name = "size") val size: Long = 0,
|
||||||
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,
|
||||||
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
|
@Json(name = "thumbnail_url") val thumbnailUrl: String? = null
|
||||||
|
@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageAudioContent(
|
data class MessageAudioContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "info") val info: AudioInfo? = null,
|
@Json(name = "info") val info: AudioInfo? = null,
|
||||||
@Json(name = "url") val url: String? = null
|
@Json(name = "url") val url: String? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -16,8 +16,13 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.message
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
|
|
||||||
interface MessageContent {
|
interface MessageContent {
|
||||||
val type: String
|
val type: String
|
||||||
val body: String
|
val body: String
|
||||||
|
val relatesTo: RelationDefaultContent?
|
||||||
|
val newContent: Content?
|
||||||
}
|
}
|
@ -18,9 +18,13 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageDefaultContent(
|
data class MessageDefaultContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String
|
@Json(name = "body") override val body: String,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageEmoteContent(
|
data class MessageEmoteContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "format") val format: String? = null,
|
@Json(name = "format") val format: String? = null,
|
||||||
@Json(name = "formatted_body") val formattedBody: String? = null
|
@Json(name = "formatted_body") val formattedBody: String? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -18,6 +18,8 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageFileContent(
|
data class MessageFileContent(
|
||||||
@ -25,5 +27,7 @@ data class MessageFileContent(
|
|||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "filename") val filename: String? = null,
|
@Json(name = "filename") val filename: String? = null,
|
||||||
@Json(name = "info") val info: FileInfo? = null,
|
@Json(name = "info") val info: FileInfo? = null,
|
||||||
@Json(name = "url") val url: String? = null
|
@Json(name = "url") val url: String? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageImageContent(
|
data class MessageImageContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "info") val info: ImageInfo? = null,
|
@Json(name = "info") val info: ImageInfo? = null,
|
||||||
@Json(name = "url") val url: String? = null
|
@Json(name = "url") val url: String? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageLocationContent(
|
data class MessageLocationContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "geo_uri") val geoUri: String,
|
@Json(name = "geo_uri") val geoUri: String,
|
||||||
@Json(name = "info") val info: LocationInfo? = null
|
@Json(name = "info") val info: LocationInfo? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageNoticeContent(
|
data class MessageNoticeContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "format") val format: String? = null,
|
@Json(name = "format") val format: String? = null,
|
||||||
@Json(name = "formatted_body") val formattedBody: String? = null
|
@Json(name = "formatted_body") val formattedBody: String? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageTextContent(
|
data class MessageTextContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "format") val format: String? = null,
|
@Json(name = "format") val format: String? = null,
|
||||||
@Json(name = "formatted_body") val formattedBody: String? = null
|
@Json(name = "formatted_body") val formattedBody: String? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Content
|
||||||
|
import im.vector.matrix.android.api.session.room.model.annotation.RelationDefaultContent
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageVideoContent(
|
data class MessageVideoContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val type: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "info") val info: VideoInfo? = null,
|
@Json(name = "info") val info: VideoInfo? = null,
|
||||||
@Json(name = "url") val url: String? = null
|
@Json(name = "url") val url: String? = null,
|
||||||
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
@ -1,5 +1,6 @@
|
|||||||
package im.vector.matrix.android.internal.database.mapper
|
package im.vector.matrix.android.internal.database.mapper
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
|
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
|
||||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
@ -16,6 +17,13 @@ internal object EventAnnotationsSummaryMapper {
|
|||||||
it.firstTimestamp,
|
it.firstTimestamp,
|
||||||
it.sourceEvents.toList()
|
it.sourceEvents.toList()
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
editSummary = annotationsSummary.editSummary?.let {
|
||||||
|
EditAggregatedSummary(
|
||||||
|
ContentMapper.map(it.aggregatedContent),
|
||||||
|
it.sourceEvents.toList(),
|
||||||
|
it.lastEditTs
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.model
|
||||||
|
|
||||||
|
import io.realm.RealmList
|
||||||
|
import io.realm.RealmObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep the latest state of edition of a message
|
||||||
|
*/
|
||||||
|
internal open class EditAggregatedSummaryEntity(
|
||||||
|
var aggregatedContent: String? = null,
|
||||||
|
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||||
|
var sourceEvents: RealmList<String> = RealmList(),
|
||||||
|
var lastEditTs: Long = 0
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
companion object
|
||||||
|
|
||||||
|
}
|
@ -24,7 +24,8 @@ internal open class EventAnnotationsSummaryEntity(
|
|||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
var eventId: String = "",
|
var eventId: String = "",
|
||||||
var roomId: String? = null,
|
var roomId: String? = null,
|
||||||
var reactionsSummary: RealmList<ReactionAggregatedSummaryEntity> = RealmList()
|
var reactionsSummary: RealmList<ReactionAggregatedSummaryEntity> = RealmList(),
|
||||||
|
var editSummary: EditAggregatedSummaryEntity? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
@ -35,6 +35,7 @@ import io.realm.annotations.RealmModule
|
|||||||
SyncEntity::class,
|
SyncEntity::class,
|
||||||
UserEntity::class,
|
UserEntity::class,
|
||||||
EventAnnotationsSummaryEntity::class,
|
EventAnnotationsSummaryEntity::class,
|
||||||
ReactionAggregatedSummaryEntity::class
|
ReactionAggregatedSummaryEntity::class,
|
||||||
|
EditAggregatedSummaryEntity::class
|
||||||
])
|
])
|
||||||
internal class SessionRealmModule
|
internal class SessionRealmModule
|
||||||
|
@ -17,19 +17,7 @@
|
|||||||
package im.vector.matrix.android.internal.di
|
package im.vector.matrix.android.internal.di
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.model.annotation.RelationContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
|
||||||
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.model.message.MessageEmoteContent
|
|
||||||
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.MessageLocationContent
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
|
|
||||||
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.internal.network.parsing.RuntimeJsonAdapterFactory
|
import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory
|
||||||
import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
|
import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
|
||||||
import im.vector.matrix.android.internal.session.sync.model.UserAccountData
|
import im.vector.matrix.android.internal.session.sync.model.UserAccountData
|
||||||
|
@ -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? {
|
||||||
|
@ -1,16 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.events.model.*
|
import im.vector.matrix.android.api.session.events.model.*
|
||||||
import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent
|
import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent
|
||||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||||
|
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 io.realm.Realm
|
import io.realm.Realm
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acts as a listener of incoming messages in order to incrementally computes a summary of annotations.
|
||||||
|
* For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity.
|
||||||
|
* The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
|
||||||
|
*/
|
||||||
internal class EventRelationsAggregationUpdater(private val credentials: Credentials) {
|
internal class EventRelationsAggregationUpdater(private val credentials: Credentials) {
|
||||||
|
|
||||||
fun update(realm: Realm, roomId: String, events: List<Event>?) {
|
fun update(realm: Realm, roomId: String, events: List<Event>?) {
|
||||||
@ -22,15 +43,64 @@ internal class EventRelationsAggregationUpdater(private val credentials: Credent
|
|||||||
handleReaction(event, roomId, realm)
|
handleReaction(event, roomId, realm)
|
||||||
}
|
}
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
event.unsignedData?.relations?.annotations?.let {
|
if (event.unsignedData?.relations?.annotations != null) {
|
||||||
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||||
handleInitialAggregatedRelations(event, roomId, it, realm)
|
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||||
}
|
} else {
|
||||||
//TODO message edits
|
val content: MessageContent? = event.content.toModel()
|
||||||
|
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||||
|
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||||
|
//A replace!
|
||||||
|
handleReplace(event, content, roomId, realm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReplace(event: Event, content: MessageContent, roomId: String, realm: Realm) {
|
||||||
|
val eventId = event.eventId ?: return
|
||||||
|
val targetEventId = content.relatesTo?.eventId ?: return
|
||||||
|
val newContent = content.newContent ?: return
|
||||||
|
//ok, this is a replace
|
||||||
|
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
||||||
|
if (existing == null) {
|
||||||
|
Timber.v("###REPLACE creating no relation summary for ${targetEventId}")
|
||||||
|
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId)
|
||||||
|
existing.roomId = roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
//we have it
|
||||||
|
val existingSummary = existing.editSummary
|
||||||
|
if (existingSummary == null) {
|
||||||
|
Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one")
|
||||||
|
//create the edit summary
|
||||||
|
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
|
||||||
|
editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||||
|
editSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||||
|
editSummary.sourceEvents.add(eventId)
|
||||||
|
|
||||||
|
existing.editSummary = editSummary
|
||||||
|
} else {
|
||||||
|
if (existingSummary.sourceEvents.contains(eventId)) {
|
||||||
|
//ignore this event, we already know it (??)
|
||||||
|
Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//This message has already been edited
|
||||||
|
if (event.originServerTs ?: 0 > existingSummary.lastEditTs ?: 0) {
|
||||||
|
Timber.v("###REPLACE Computing aggregated edit summary")
|
||||||
|
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||||
|
existingSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||||
|
existingSummary.sourceEvents.add(eventId)
|
||||||
|
} else {
|
||||||
|
//ignore this event for the summary
|
||||||
|
Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
||||||
aggregation.chunk?.forEach {
|
aggregation.chunk?.forEach {
|
||||||
@ -52,7 +122,7 @@ internal class EventRelationsAggregationUpdater(private val credentials: Credent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReaction(event: Event, roomId: String, realm: Realm) {
|
fun handleReaction(event: Event, roomId: String, realm: Realm) {
|
||||||
event.content.toModel<ReactionContent>()?.let { content ->
|
event.content.toModel<ReactionContent>()?.let { content ->
|
||||||
//rel_type must be m.annotation
|
//rel_type must be m.annotation
|
||||||
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
||||||
@ -82,4 +152,78 @@ internal class EventRelationsAggregationUpdater(private val credentials: Credent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an event is deleted
|
||||||
|
*/
|
||||||
|
fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
|
||||||
|
Timber.d("Handle redaction of m.replace")
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
|
||||||
|
if (eventSummary == null) {
|
||||||
|
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val sourceEvents = eventSummary.editSummary?.sourceEvents
|
||||||
|
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId)
|
||||||
|
if (sourceToDiscard == null) {
|
||||||
|
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//Need to remove this event from the redaction list and compute new aggregation state
|
||||||
|
sourceEvents.removeAt(sourceToDiscard)
|
||||||
|
val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull()
|
||||||
|
if (previousEdit == null) {
|
||||||
|
//revert to original
|
||||||
|
eventSummary.editSummary?.deleteFromRealm()
|
||||||
|
} else {
|
||||||
|
//I have the last event
|
||||||
|
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
|
||||||
|
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs
|
||||||
|
?: System.currentTimeMillis()
|
||||||
|
eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent)
|
||||||
|
} ?: run {
|
||||||
|
Timber.e("Failed to udate edited summary")
|
||||||
|
//TODO how to reccover that
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
|
||||||
|
Timber.d("REDACTION of reaction ${eventToPrune.eventId}")
|
||||||
|
//delete a reaction, need to update the annotation summary if any
|
||||||
|
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
|
||||||
|
?: return
|
||||||
|
val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return
|
||||||
|
|
||||||
|
val reactionkey = reactionContent.relatesTo.key
|
||||||
|
Timber.d("REMOVE reaction for key $reactionkey")
|
||||||
|
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
|
||||||
|
if (summary != null) {
|
||||||
|
summary.reactionsSummary.where()
|
||||||
|
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey)
|
||||||
|
.findFirst()?.let { summary ->
|
||||||
|
Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})")
|
||||||
|
Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}")
|
||||||
|
if (summary.sourceEvents.contains(eventToPrune.eventId)) {
|
||||||
|
Timber.d("REMOVE reaction for key $reactionkey")
|
||||||
|
summary.sourceEvents.remove(eventToPrune.eventId)
|
||||||
|
Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}")
|
||||||
|
summary.count = summary.count - 1
|
||||||
|
if (eventToPrune.sender == userId) {
|
||||||
|
//Was it a redact on my reaction?
|
||||||
|
summary.addedByMe = false
|
||||||
|
}
|
||||||
|
if (summary.count == 0) {
|
||||||
|
//delete!
|
||||||
|
summary.deleteFromRealm()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("## Cannot find summary for key $reactionkey")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -109,7 +109,7 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
DefaultPruneEventTask(get()) as PruneEventTask
|
DefaultPruneEventTask(get(),get()) as PruneEventTask
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,18 +17,14 @@ package im.vector.matrix.android.internal.session.room.prune
|
|||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.*
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
|
||||||
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.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
@ -44,7 +40,9 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultPruneEventTask(private val monarchy: Monarchy) : PruneEventTask {
|
internal class DefaultPruneEventTask(
|
||||||
|
private val monarchy: Monarchy,
|
||||||
|
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) : PruneEventTask {
|
||||||
|
|
||||||
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
||||||
return monarchy.tryTransactionSync { realm ->
|
return monarchy.tryTransactionSync { realm ->
|
||||||
@ -72,51 +70,25 @@ internal class DefaultPruneEventTask(private val monarchy: Monarchy) : PruneEven
|
|||||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||||
?: UnsignedData(null, null)
|
?: 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) {
|
||||||
|
eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||||
|
}
|
||||||
|
|
||||||
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
||||||
eventToPrune.content = ContentMapper.map(emptyMap())
|
eventToPrune.content = ContentMapper.map(emptyMap())
|
||||||
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
||||||
|
|
||||||
}
|
}
|
||||||
EventType.REACTION -> {
|
EventType.REACTION -> {
|
||||||
Timber.d("REDACTION of reaction ${eventToPrune.eventId}")
|
eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
|
||||||
//delete a reaction, need to update the annotation summary if any
|
}
|
||||||
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
|
}
|
||||||
?: return
|
}
|
||||||
val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return
|
}
|
||||||
|
|
||||||
val reactionkey = reactionContent.relatesTo.key
|
|
||||||
Timber.d("REMOVE reaction for key $reactionkey")
|
|
||||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
|
|
||||||
if (summary != null) {
|
|
||||||
summary.reactionsSummary.where()
|
|
||||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey)
|
|
||||||
.findFirst()?.let { summary ->
|
|
||||||
Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})")
|
|
||||||
Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}")
|
|
||||||
if (summary.sourceEvents.contains(eventToPrune.eventId)) {
|
|
||||||
Timber.d("REMOVE reaction for key $reactionkey")
|
|
||||||
summary.sourceEvents.remove(eventToPrune.eventId)
|
|
||||||
Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}")
|
|
||||||
summary.count = summary.count - 1
|
|
||||||
if (eventToPrune.sender == userId) {
|
|
||||||
//Was it a redact on my reaction?
|
|
||||||
summary.addedByMe = false
|
|
||||||
}
|
|
||||||
if (summary.count == 0) {
|
|
||||||
//delete!
|
|
||||||
summary.deleteFromRealm()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.e("## Cannot find summary for key $reactionkey")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun computeAllowedKeys(type: String): List<String> {
|
private fun computeAllowedKeys(type: String): List<String> {
|
||||||
// Add filtered content, allowed keys in content depends on the event type
|
// Add filtered content, allowed keys in content depends on the event type
|
||||||
|
@ -19,7 +19,6 @@ package im.vector.riotredesign.core.di
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import im.vector.matrix.android.api.Matrix
|
import im.vector.matrix.android.api.Matrix
|
||||||
import im.vector.riotredesign.core.resources.ColorProvider
|
|
||||||
import im.vector.riotredesign.core.resources.LocaleProvider
|
import im.vector.riotredesign.core.resources.LocaleProvider
|
||||||
import im.vector.riotredesign.core.resources.StringProvider
|
import im.vector.riotredesign.core.resources.StringProvider
|
||||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
||||||
@ -41,10 +40,6 @@ class AppModule(private val context: Context) {
|
|||||||
StringProvider(context.resources)
|
StringProvider(context.resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
|
||||||
ColorProvider(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
single {
|
single {
|
||||||
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
|
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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()),
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 ?: "",
|
||||||
|
@ -17,12 +17,19 @@
|
|||||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
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
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
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.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
|
||||||
@ -73,6 +80,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
textColor = colorProvider.getColor(AvatarRenderer.getColorFromUserId(event.root.sender
|
textColor = colorProvider.getColor(AvatarRenderer.getColorFromUserId(event.root.sender
|
||||||
?: ""))
|
?: ""))
|
||||||
}
|
}
|
||||||
|
val hasBeenEdited = event.annotations?.editSummary != null
|
||||||
val informationData = MessageInformationData(eventId = eventId,
|
val informationData = MessageInformationData(eventId = eventId,
|
||||||
senderId = event.root.sender ?: "",
|
senderId = event.root.sender ?: "",
|
||||||
sendState = event.sendState,
|
sendState = event.sendState,
|
||||||
@ -80,7 +88,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
showInformation = showInformation,
|
showInformation = showInformation,
|
||||||
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) }
|
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) },
|
||||||
|
hasBeenEdited = hasBeenEdited
|
||||||
)
|
)
|
||||||
|
|
||||||
if (event.root.unsignedData?.redactedEvent != null) {
|
if (event.root.unsignedData?.redactedEvent != null) {
|
||||||
@ -88,13 +97,31 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
return buildRedactedItem(informationData, callback)
|
return buildRedactedItem(informationData, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
val messageContent: MessageContent =
|
||||||
|
event.annotations?.editSummary?.aggregatedContent?.toModel()
|
||||||
|
?: event.root.content.toModel()
|
||||||
|
?: return null
|
||||||
|
|
||||||
|
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
||||||
|
//TODO blank item or ignore??
|
||||||
|
// ignore this event
|
||||||
|
return BlankItem_()
|
||||||
|
}
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// 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,
|
||||||
is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback)
|
informationData,
|
||||||
|
hasBeenEdited,
|
||||||
|
event.annotations?.editSummary,
|
||||||
|
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)
|
||||||
@ -254,6 +281,8 @@ 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,
|
||||||
|
editSummary: EditAggregatedSummary?,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
|
||||||
val bodyToUse = messageContent.formattedBody?.let {
|
val bodyToUse = messageContent.formattedBody?.let {
|
||||||
@ -261,8 +290,16 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
} ?: messageContent.body
|
} ?: messageContent.body
|
||||||
|
|
||||||
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
||||||
|
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(linkifiedBody)
|
.apply {
|
||||||
|
if (hasBeenEdited) {
|
||||||
|
val spannable = annotateWithEdited(linkifiedBody, callback, informationData, editSummary)
|
||||||
|
message(spannable)
|
||||||
|
} else {
|
||||||
|
message(linkifiedBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
.avatarClickListener(
|
.avatarClickListener(
|
||||||
@ -288,6 +325,39 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
||||||
|
callback: TimelineEventController.Callback?,
|
||||||
|
informationData: MessageInformationData,
|
||||||
|
editSummary: EditAggregatedSummary?): SpannableStringBuilder {
|
||||||
|
val spannable = SpannableStringBuilder()
|
||||||
|
spannable.append(linkifiedBody)
|
||||||
|
val editedSuffix = "(edited)"
|
||||||
|
spannable.append(" ").append(editedSuffix)
|
||||||
|
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)
|
||||||
|
return spannable
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, informationData: MessageInformationData,
|
private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, informationData: MessageInformationData,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
|
||||||
@ -322,6 +392,8 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, informationData: MessageInformationData,
|
private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, informationData: MessageInformationData,
|
||||||
|
hasBeenEdited: Boolean,
|
||||||
|
editSummary: EditAggregatedSummary?,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
|
||||||
val message = messageContent.body.let {
|
val message = messageContent.body.let {
|
||||||
@ -329,7 +401,14 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
linkifyBody(formattedBody, callback)
|
linkifyBody(formattedBody, callback)
|
||||||
}
|
}
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(message)
|
.apply {
|
||||||
|
if (hasBeenEdited) {
|
||||||
|
val spannable = annotateWithEdited(message, callback, informationData, editSummary)
|
||||||
|
message(spannable)
|
||||||
|
} else {
|
||||||
|
message(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
.avatarClickListener(
|
.avatarClickListener(
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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.riotredesign.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
|
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_blank_stub)
|
||||||
|
abstract class BlankItem : VectorEpoxyModel<BlankItem.BlankHolder>() {
|
||||||
|
class BlankHolder : VectorEpoxyHolder()
|
||||||
|
}
|
@ -31,5 +31,6 @@ data class MessageInformationData(
|
|||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
val showInformation: Boolean = true,
|
val showInformation: Boolean = true,
|
||||||
/*List of reactions (emoji,count,isSelected)*/
|
/*List of reactions (emoji,count,isSelected)*/
|
||||||
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null
|
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null,
|
||||||
|
var hasBeenEdited: Boolean = false
|
||||||
) : Parcelable
|
) : Parcelable
|
@ -32,6 +32,8 @@
|
|||||||
android:id="@+id/messageContentBlankStub"
|
android:id="@+id/messageContentBlankStub"
|
||||||
style="@style/TimelineContentStubNoInfoLayoutParams"
|
style="@style/TimelineContentStubNoInfoLayoutParams"
|
||||||
android:layout="@layout/item_timeline_event_blank_stub"
|
android:layout="@layout/item_timeline_event_blank_stub"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
<ViewStub
|
<ViewStub
|
||||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user