mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Reply In Thread, create a new thread timeline
This commit is contained in:
parent
cb0fefa74d
commit
ecc9b59ad1
26 changed files with 340 additions and 154 deletions
|
@ -96,6 +96,9 @@ data class Event(
|
||||||
@Transient
|
@Transient
|
||||||
var sendStateDetails: String? = null
|
var sendStateDetails: String? = null
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var isRootThread: Boolean = false
|
||||||
|
|
||||||
fun sendStateError(): MatrixError? {
|
fun sendStateError(): MatrixError? {
|
||||||
return sendStateDetails?.let {
|
return sendStateDetails?.let {
|
||||||
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
||||||
|
@ -299,12 +302,22 @@ fun Event.getRelationContent(): RelationDefaultContent? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the relation content for a specific type or null otherwise
|
||||||
|
*/
|
||||||
|
fun Event.getRelationContentForType(type: String): RelationDefaultContent? =
|
||||||
|
getRelationContent()?.takeIf { it.type == type }
|
||||||
|
|
||||||
fun Event.isReply(): Boolean {
|
fun Event.isReply(): Boolean {
|
||||||
return getRelationContent()?.inReplyTo?.eventId != null
|
return getRelationContent()?.inReplyTo?.eventId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
|
||||||
|
|
||||||
|
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
|
||||||
|
|
||||||
fun Event.isEdition(): Boolean {
|
fun Event.isEdition(): Boolean {
|
||||||
return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
|
return getRelationContentForType(RelationType.REPLACE)?.eventId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Event.getPresenceContent(): PresenceContent? {
|
fun Event.getPresenceContent(): PresenceContent? {
|
||||||
|
|
|
@ -131,11 +131,11 @@ interface RelationService {
|
||||||
* Creates a thread reply for an existing timeline event
|
* Creates a thread reply for an existing timeline event
|
||||||
* The replyInThreadText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
|
* The replyInThreadText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
|
||||||
* by the sdk into pills.
|
* by the sdk into pills.
|
||||||
* @param eventToReplyInThread the event referenced by the thread reply
|
* @param rootThreadEventId the root thread eventId
|
||||||
* @param replyInThreadText the reply text
|
* @param replyInThreadText the reply text
|
||||||
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||||
*/
|
*/
|
||||||
fun replyInThread(eventToReplyInThread: TimelineEvent,
|
fun replyInThread(rootThreadEventId: String,
|
||||||
replyInThreadText: CharSequence,
|
replyInThreadText: CharSequence,
|
||||||
autoMarkdown: Boolean = false): Cancelable?
|
autoMarkdown: Boolean = false): Cancelable?
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.api.session.room.model.relation.threads
|
|
||||||
|
|
||||||
interface ThreadContent {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val MSG_TYPE_JSON_KEY = "msgtype"
|
|
||||||
}
|
|
||||||
|
|
||||||
val msgType: String
|
|
||||||
val body: String
|
|
||||||
val relatesTo: ThreadRelatesTo?
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2021 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 org.matrix.android.sdk.api.session.room.model.relation.threads
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class ThreadRelatesTo(
|
|
||||||
@Json(name = "rel_type") override val type: String? = RelationType.THREAD,
|
|
||||||
@Json(name = "event_id") override val eventId: String,
|
|
||||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
|
|
||||||
@Json(name = "option") override val option: Int? = null
|
|
||||||
) : RelationContent
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
||||||
*
|
|
||||||
* 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 org.matrix.android.sdk.api.session.room.model.relation.threads
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class ThreadTextContent(
|
|
||||||
@Json(name = MessageContent.MSG_TYPE_JSON_KEY) override val msgType: String,
|
|
||||||
@Json(name = "body") override val body: String,
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: ThreadRelatesTo? = null,
|
|
||||||
) : ThreadContent
|
|
|
@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.VersioningState
|
import org.matrix.android.sdk.api.session.room.model.VersioningState
|
||||||
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||||
|
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
|
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
|
||||||
|
@ -49,7 +50,7 @@ import timber.log.Timber
|
||||||
|
|
||||||
internal object RealmSessionStoreMigration : RealmMigration {
|
internal object RealmSessionStoreMigration : RealmMigration {
|
||||||
|
|
||||||
const val SESSION_STORE_SCHEMA_VERSION = 18L
|
const val SESSION_STORE_SCHEMA_VERSION = 19L
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
|
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
|
||||||
|
@ -72,6 +73,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
|
||||||
if (oldVersion <= 15) migrateTo16(realm)
|
if (oldVersion <= 15) migrateTo16(realm)
|
||||||
if (oldVersion <= 16) migrateTo17(realm)
|
if (oldVersion <= 16) migrateTo17(realm)
|
||||||
if (oldVersion <= 17) migrateTo18(realm)
|
if (oldVersion <= 17) migrateTo18(realm)
|
||||||
|
if (oldVersion <= 18) migrateTo19(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1(realm: DynamicRealm) {
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
|
@ -364,4 +366,13 @@ internal object RealmSessionStoreMigration : RealmMigration {
|
||||||
realm.schema.get("RoomMemberSummaryEntity")
|
realm.schema.get("RoomMemberSummaryEntity")
|
||||||
?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
|
?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun migrateTo19(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 18 -> 19")
|
||||||
|
realm.schema.get("EventEntity")
|
||||||
|
?.addField(EventEntityFields.IS_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
||||||
|
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java)
|
||||||
|
realm.schema.get("ChunkEntity")
|
||||||
|
?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isThread
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
@ -39,6 +41,8 @@ internal object EventMapper {
|
||||||
eventEntity.isUseless = IsUselessResolver.isUseless(event)
|
eventEntity.isUseless = IsUselessResolver.isUseless(event)
|
||||||
eventEntity.stateKey = event.stateKey
|
eventEntity.stateKey = event.stateKey
|
||||||
eventEntity.type = event.type ?: EventType.MISSING_TYPE
|
eventEntity.type = event.type ?: EventType.MISSING_TYPE
|
||||||
|
eventEntity.isThread = if(event.isRootThread) true else event.isThread()
|
||||||
|
eventEntity.rootThreadEventId = if(event.isRootThread) null else event.getRootThreadEventId()
|
||||||
eventEntity.sender = event.senderId
|
eventEntity.sender = event.senderId
|
||||||
eventEntity.originServerTs = event.originServerTs
|
eventEntity.originServerTs = event.originServerTs
|
||||||
eventEntity.redacts = event.redacts
|
eventEntity.redacts = event.redacts
|
||||||
|
@ -93,6 +97,7 @@ internal object EventMapper {
|
||||||
MXCryptoError.ErrorType.valueOf(errorCode)
|
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||||
}
|
}
|
||||||
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
|
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
|
||||||
|
it.isRootThread = eventEntity.isRootThread()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.extensions.clearWith
|
||||||
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||||
// Because of gaps we can have several chunks with nextToken == null
|
// Because of gaps we can have several chunks with nextToken == null
|
||||||
@Index var nextToken: String? = null,
|
@Index var nextToken: String? = null,
|
||||||
|
@Index var rootThreadEventId: String? = null,
|
||||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||||
var numberOfTimelineEvents: Long = 0,
|
var numberOfTimelineEvents: Long = 0,
|
||||||
|
@ -44,6 +45,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||||
val room: RealmResults<RoomEntity>? = null
|
val room: RealmResults<RoomEntity>? = null
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
|
||||||
|
fun isThreadChunk() = rootThreadEventId != null
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
|
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
|
||||||
|
|
|
@ -27,6 +27,8 @@ import org.matrix.android.sdk.internal.extensions.assertIsManaged
|
||||||
internal open class EventEntity(@Index var eventId: String = "",
|
internal open class EventEntity(@Index var eventId: String = "",
|
||||||
@Index var roomId: String = "",
|
@Index var roomId: String = "",
|
||||||
@Index var type: String = "",
|
@Index var type: String = "",
|
||||||
|
@Index var isThread: Boolean = false,
|
||||||
|
var rootThreadEventId: String? = null,
|
||||||
var content: String? = null,
|
var content: String? = null,
|
||||||
var prevContent: String? = null,
|
var prevContent: String? = null,
|
||||||
var isUseless: Boolean = false,
|
var isUseless: Boolean = false,
|
||||||
|
@ -75,4 +77,10 @@ internal open class EventEntity(@Index var eventId: String = "",
|
||||||
.findFirst()
|
.findFirst()
|
||||||
?.canBeProcessed = true
|
?.canBeProcessed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the current event is a thread root event
|
||||||
|
*/
|
||||||
|
fun isRootThread(): Boolean = isThread && rootThreadEventId == null
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,11 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
|
||||||
val query = where(realm, roomId)
|
val query = where(realm, roomId)
|
||||||
if (prevToken != null) {
|
if (prevToken != null) {
|
||||||
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
|
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
|
||||||
|
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||||
}
|
}
|
||||||
if (nextToken != null) {
|
if (nextToken != null) {
|
||||||
query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken)
|
query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken)
|
||||||
|
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||||
}
|
}
|
||||||
return query.findFirst()
|
return query.findFirst()
|
||||||
}
|
}
|
||||||
|
@ -43,12 +45,15 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
|
||||||
internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? {
|
internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? {
|
||||||
return where(realm, roomId)
|
return where(realm, roomId)
|
||||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||||
|
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
|
internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
|
||||||
return realm.where<ChunkEntity>()
|
return realm.where<ChunkEntity>()
|
||||||
.`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
|
.`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
|
||||||
|
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||||
.findAll()
|
.findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +61,7 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
|
||||||
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
|
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun ChunkEntity.Companion.create(
|
internal fun ChunkEntity.Companion.create(
|
||||||
realm: Realm,
|
realm: Realm,
|
||||||
prevToken: String?,
|
prevToken: String?,
|
||||||
|
@ -66,3 +72,16 @@ internal fun ChunkEntity.Companion.create(
|
||||||
this.nextToken = nextToken
|
this.nextToken = nextToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Threads
|
||||||
|
internal fun ChunkEntity.Companion.findThreadChunkOfRoom(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? {
|
||||||
|
return where(realm, roomId)
|
||||||
|
.equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ChunkEntity.Companion.findAllThreadChunkOfRoom(realm: Realm, roomId: String): RealmResults<ChunkEntity> {
|
||||||
|
return where(realm, roomId)
|
||||||
|
.isNotNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||||
|
.findAll()
|
||||||
|
}
|
||||||
|
|
|
@ -159,11 +159,15 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun replyInThread(eventToReplyInThread: TimelineEvent, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable? {
|
override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable {
|
||||||
val event = eventFactory.createThreadTextEvent(eventToReplyInThread, TextContent(replyInThreadText.toString()))
|
val event = eventFactory.createThreadTextEvent(
|
||||||
.also {
|
rootThreadEventId = rootThreadEventId,
|
||||||
saveLocalEcho(it)
|
roomId = roomId,
|
||||||
}
|
text = replyInThreadText.toString(),
|
||||||
|
autoMarkdown = autoMarkdown)
|
||||||
|
// .also {
|
||||||
|
// saveLocalEcho(it)
|
||||||
|
// }
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,6 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo
|
import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
|
import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.threads.ThreadTextContent
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.threads.ThreadRelatesTo
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.isReply
|
import org.matrix.android.sdk.api.session.room.timeline.isReply
|
||||||
|
@ -60,7 +58,6 @@ import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
|
import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
|
||||||
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
|
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
|
||||||
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
|
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -346,11 +343,13 @@ internal class LocalEchoEventFactory @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* Creates a thread event related to the already existing event
|
* Creates a thread event related to the already existing event
|
||||||
*/
|
*/
|
||||||
fun createThreadTextEvent(eventToReplyInThread: TimelineEvent, textContent: TextContent): Event =
|
fun createThreadTextEvent(rootThreadEventId: String, roomId:String, text: String, autoMarkdown: Boolean): Event =
|
||||||
createEvent(
|
createEvent(
|
||||||
eventToReplyInThread.roomId,
|
roomId,
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
textContent.toThreadTextContent(eventToReplyInThread).toContent())
|
createTextContent(text, autoMarkdown)
|
||||||
|
.toThreadTextContent(rootThreadEventId)
|
||||||
|
.toContent())
|
||||||
|
|
||||||
private fun dummyOriginServerTs(): Long {
|
private fun dummyOriginServerTs(): Long {
|
||||||
return System.currentTimeMillis()
|
return System.currentTimeMillis()
|
||||||
|
|
|
@ -16,13 +16,11 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.room.send
|
package org.matrix.android.sdk.internal.session.room.send
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.threads.ThreadTextContent
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.threads.ThreadRelatesTo
|
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|
||||||
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
||||||
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
|
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
|
||||||
|
|
||||||
|
@ -45,11 +43,13 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TextContent.toThreadTextContent(eventToReplyInThread: TimelineEvent, msgType: String = MessageType.MSGTYPE_TEXT): ThreadTextContent {
|
fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
|
||||||
return ThreadTextContent(
|
return MessageTextContent(
|
||||||
msgType = msgType,
|
msgType = msgType,
|
||||||
|
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
|
||||||
body = text,
|
body = text,
|
||||||
relatesTo = ThreadRelatesTo(eventId = eventToReplyInThread.eventId)
|
relatesTo = RelationDefaultContent(RelationType.THREAD, rootThreadEventId),
|
||||||
|
formattedBody = formattedText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,9 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isThread
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
@ -28,6 +30,8 @@ import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||||
import org.matrix.android.sdk.internal.database.helper.merge
|
import org.matrix.android.sdk.internal.database.helper.merge
|
||||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||||
|
@ -37,6 +41,7 @@ import org.matrix.android.sdk.internal.database.query.create
|
||||||
import org.matrix.android.sdk.internal.database.query.find
|
import org.matrix.android.sdk.internal.database.query.find
|
||||||
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
|
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
|
||||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findThreadChunkOfRoom
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
@ -221,7 +226,15 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
||||||
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
|
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
|
||||||
}
|
}
|
||||||
|
|
||||||
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
Timber.i("------> [TokenChunkEventPersistor] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}")
|
||||||
|
|
||||||
|
addTimelineEventToChunk(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
eventEntity = eventEntity,
|
||||||
|
currentChunk = currentChunk,
|
||||||
|
direction = direction,
|
||||||
|
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||||
}
|
}
|
||||||
// Find all the chunks which contain at least one event from the list of eventIds
|
// Find all the chunks which contain at least one event from the list of eventIds
|
||||||
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
||||||
|
@ -247,4 +260,43 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
||||||
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
|
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a timeline event to the correct chunk. If there is a thread detected will be added
|
||||||
|
* to a specific chunk
|
||||||
|
*/
|
||||||
|
private fun addTimelineEventToChunk(realm: Realm,
|
||||||
|
roomId: String,
|
||||||
|
eventEntity: EventEntity,
|
||||||
|
currentChunk: ChunkEntity,
|
||||||
|
direction: PaginationDirection,
|
||||||
|
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
|
||||||
|
val rootThreadEventId = eventEntity.rootThreadEventId
|
||||||
|
if (eventEntity.isThread && rootThreadEventId != null) {
|
||||||
|
val threadChunk = getOrCreateThreadChunk(realm, roomId, rootThreadEventId)
|
||||||
|
threadChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||||
|
markEventAsRootEvent(realm, rootThreadEventId)
|
||||||
|
if (threadChunk.isValid)
|
||||||
|
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(threadChunk)
|
||||||
|
} else {
|
||||||
|
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) {
|
||||||
|
val rootThreadEvent = EventEntity
|
||||||
|
.where(realm, rootThreadEventId)
|
||||||
|
.equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return
|
||||||
|
rootThreadEvent.isThread = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity
|
||||||
|
*/
|
||||||
|
private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity {
|
||||||
|
return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId)
|
||||||
|
?: realm.createObject<ChunkEntity>().apply {
|
||||||
|
this.rootThreadEventId = rootThreadEventId
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,31 +21,41 @@ import io.realm.kotlin.createObject
|
||||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isThread
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.initsync.InitSyncStep
|
import org.matrix.android.sdk.api.session.initsync.InitSyncStep
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
|
import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
|
||||||
import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
|
import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
|
||||||
import org.matrix.android.sdk.api.session.sync.model.RoomSync
|
import org.matrix.android.sdk.api.session.sync.model.RoomSync
|
||||||
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
|
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
|
||||||
|
import org.matrix.android.sdk.api.util.JsonDict
|
||||||
|
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||||
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import org.matrix.android.sdk.internal.database.helper.addIfNecessary
|
import org.matrix.android.sdk.internal.database.helper.addIfNecessary
|
||||||
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.EventMapper
|
||||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
||||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||||
import org.matrix.android.sdk.internal.database.query.find
|
import org.matrix.android.sdk.internal.database.query.find
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findAllThreadChunkOfRoom
|
||||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findThreadChunkOfRoom
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrNull
|
import org.matrix.android.sdk.internal.database.query.getOrNull
|
||||||
import org.matrix.android.sdk.internal.database.query.where
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
@ -58,8 +68,11 @@ import org.matrix.android.sdk.internal.session.initsync.mapWithProgress
|
||||||
import org.matrix.android.sdk.internal.session.initsync.reportSubtask
|
import org.matrix.android.sdk.internal.session.initsync.reportSubtask
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
|
||||||
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
|
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
|
||||||
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||||
|
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
||||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
|
import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
|
||||||
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
|
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
|
||||||
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
|
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
|
||||||
|
@ -356,6 +369,21 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
val eventIds = ArrayList<String>(eventList.size)
|
val eventIds = ArrayList<String>(eventList.size)
|
||||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// There is only one chunk per room
|
||||||
|
|
||||||
|
val threadChunks = ChunkEntity.findAllThreadChunkOfRoom(realm, roomId)
|
||||||
|
|
||||||
|
val tc = threadChunks.joinToString { chunk ->
|
||||||
|
var output = "\n----------------\n------> [${chunk.timelineEvents.size}] rootThreadEventId = ${chunk.rootThreadEventId}" + "\n"
|
||||||
|
output += chunk.timelineEvents
|
||||||
|
.joinToString("") {
|
||||||
|
"------> " + "eventId:[${it?.eventId}] payload:[${getValueFromPayload(it.root?.let { root -> EventMapper.map(root).mxDecryptionResult }?.payload, "body")}]\n"
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
Timber.i("------> Chunks (${threadChunks.size})$tc")
|
||||||
|
/////////////////////
|
||||||
for (event in eventList) {
|
for (event in eventList) {
|
||||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||||
continue
|
continue
|
||||||
|
@ -385,7 +413,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
Timber.i("------> [RoomSyncHandler] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}")
|
||||||
|
|
||||||
|
addTimelineEventToChunk(
|
||||||
|
realm = realm,
|
||||||
|
roomId = roomId,
|
||||||
|
eventEntity = eventEntity,
|
||||||
|
chunkEntity = chunkEntity,
|
||||||
|
roomEntity = roomEntity,
|
||||||
|
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||||
|
|
||||||
// Give info to crypto module
|
// Give info to crypto module
|
||||||
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
||||||
|
|
||||||
|
@ -412,9 +449,54 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
}
|
}
|
||||||
// posting new events to timeline if any is registered
|
// posting new events to timeline if any is registered
|
||||||
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
||||||
|
|
||||||
return chunkEntity
|
return chunkEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a timeline event to the correct chunk. If there is a thread detected will be added
|
||||||
|
* to a specific chunk
|
||||||
|
*/
|
||||||
|
private fun addTimelineEventToChunk(realm: Realm,
|
||||||
|
roomId: String,
|
||||||
|
eventEntity: EventEntity,
|
||||||
|
chunkEntity: ChunkEntity,
|
||||||
|
roomEntity: RoomEntity,
|
||||||
|
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
|
||||||
|
val rootThreadEventId = eventEntity.rootThreadEventId
|
||||||
|
if (eventEntity.isThread && rootThreadEventId != null) {
|
||||||
|
val threadChunk = getOrCreateThreadChunk(realm, roomId, rootThreadEventId)
|
||||||
|
threadChunk.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||||
|
markEventAsRootEvent(realm, rootThreadEventId)
|
||||||
|
roomEntity.addIfNecessary(threadChunk)
|
||||||
|
} else {
|
||||||
|
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
|
||||||
|
val content = payload?.get("content") as? JsonDict
|
||||||
|
return content?.get(key) as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity
|
||||||
|
*/
|
||||||
|
private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity {
|
||||||
|
return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId)
|
||||||
|
?: realm.createObject<ChunkEntity>().apply {
|
||||||
|
this.rootThreadEventId = rootThreadEventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String){
|
||||||
|
val rootThreadEvent = EventEntity
|
||||||
|
.where(realm, rootThreadEventId)
|
||||||
|
.equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return
|
||||||
|
rootThreadEvent.isThread = true
|
||||||
|
}
|
||||||
|
|
||||||
private fun decryptIfNeeded(event: Event, roomId: String) {
|
private fun decryptIfNeeded(event: Event, roomId: String) {
|
||||||
try {
|
try {
|
||||||
// Event from sync does not have roomId, so add it to the event first
|
// Event from sync does not have roomId, so add it to the event first
|
||||||
|
|
|
@ -49,6 +49,7 @@ import androidx.core.text.toSpannable
|
||||||
import androidx.core.util.Pair
|
import androidx.core.util.Pair
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.setFragmentResultListener
|
import androidx.fragment.app.setFragmentResultListener
|
||||||
|
@ -228,7 +229,8 @@ data class RoomDetailArgs(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String? = null,
|
val eventId: String? = null,
|
||||||
val sharedData: SharedData? = null,
|
val sharedData: SharedData? = null,
|
||||||
val openShareSpaceForId: String? = null
|
val openShareSpaceForId: String? = null,
|
||||||
|
val roomThreadDetailArgs: RoomThreadDetailArgs? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
class RoomDetailFragment @Inject constructor(
|
class RoomDetailFragment @Inject constructor(
|
||||||
|
@ -352,7 +354,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
)
|
)
|
||||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||||
lazyLoadedViews.bind(views)
|
lazyLoadedViews.bind(views)
|
||||||
|
if (isThreadTimeLine()) {
|
||||||
|
views.roomToolbar.isGone = true
|
||||||
|
} else {
|
||||||
setupToolbar(views.roomToolbar)
|
setupToolbar(views.roomToolbar)
|
||||||
|
}
|
||||||
|
setupThreadIfNeeded()
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupComposer()
|
setupComposer()
|
||||||
setupNotificationView()
|
setupNotificationView()
|
||||||
|
@ -902,6 +909,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
super.onCreateOptionsMenu(menu, inflater)
|
||||||
|
if (isThreadTimeLine()) return
|
||||||
// We use a custom layout for this menu item, so we need to set a ClickListener
|
// We use a custom layout for this menu item, so we need to set a ClickListener
|
||||||
menu.findItem(R.id.open_matrix_apps)?.let { menuItem ->
|
menu.findItem(R.id.open_matrix_apps)?.let { menuItem ->
|
||||||
menuItem.actionView.setOnClickListener {
|
menuItem.actionView.setOnClickListener {
|
||||||
|
@ -915,6 +923,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||||
|
if (isThreadTimeLine()) {
|
||||||
|
menu.forEach {
|
||||||
|
it.isVisible = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
menu.forEach {
|
menu.forEach {
|
||||||
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
|
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
|
||||||
}
|
}
|
||||||
|
@ -1180,6 +1194,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
|
private fun setupThreadIfNeeded(){
|
||||||
|
getRootThreadEventId()?.let{
|
||||||
|
textComposerViewModel.handle(TextComposerAction.EnterReplyInThreadTimeline(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
timelineEventController.callback = this
|
timelineEventController.callback = this
|
||||||
timelineEventController.timeline = roomDetailViewModel.timeline
|
timelineEventController.timeline = roomDetailViewModel.timeline
|
||||||
|
@ -2203,4 +2223,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isThreadTimeLine(): Boolean = roomDetailArgs.roomThreadDetailArgs != null
|
||||||
|
fun getRootThreadEventId(): String? = roomDetailArgs.roomThreadDetailArgs?.eventId
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,8 @@ data class RoomDetailViewState(
|
||||||
val isAllowedToManageWidgets: Boolean = false,
|
val isAllowedToManageWidgets: Boolean = false,
|
||||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||||
val hasFailedSending: Boolean = false,
|
val hasFailedSending: Boolean = false,
|
||||||
val jitsiState: JitsiState = JitsiState()
|
val jitsiState: JitsiState = JitsiState(),
|
||||||
|
val rootThreadEventId: String? = null
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(
|
constructor(args: RoomDetailArgs) : this(
|
||||||
|
|
|
@ -28,4 +28,6 @@ sealed class TextComposerAction : VectorViewModelAction {
|
||||||
data class UserIsTyping(val isTyping: Boolean) : TextComposerAction()
|
data class UserIsTyping(val isTyping: Boolean) : TextComposerAction()
|
||||||
data class OnTextChanged(val text: CharSequence) : TextComposerAction()
|
data class OnTextChanged(val text: CharSequence) : TextComposerAction()
|
||||||
data class OnVoiceRecordingStateChanged(val isRecording: Boolean) : TextComposerAction()
|
data class OnVoiceRecordingStateChanged(val isRecording: Boolean) : TextComposerAction()
|
||||||
|
data class EnterReplyInThreadTimeline(val rootThreadEventId: String) : TextComposerAction()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ import org.commonmark.renderer.html.HtmlRenderer
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
|
||||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
||||||
|
@ -88,6 +89,8 @@ class TextComposerViewModel @AssistedInject constructor(
|
||||||
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
||||||
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
||||||
is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action)
|
is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action)
|
||||||
|
is TextComposerAction.EnterReplyInThreadTimeline -> handleEnterReplyInThreadTimeline(action)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +98,10 @@ class TextComposerViewModel @AssistedInject constructor(
|
||||||
copy(isVoiceRecording = action.isRecording)
|
copy(isVoiceRecording = action.isRecording)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleEnterReplyInThreadTimeline(action: TextComposerAction.EnterReplyInThreadTimeline) = setState {
|
||||||
|
copy(rootThreadEventId = action.rootThreadEventId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) {
|
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) {
|
||||||
setState {
|
setState {
|
||||||
// Makes sure currentComposerText is upToDate when accessing further setState
|
// Makes sure currentComposerText is upToDate when accessing further setState
|
||||||
|
@ -155,7 +162,11 @@ class TextComposerViewModel @AssistedInject constructor(
|
||||||
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
|
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
|
||||||
is ParsedCommand.ErrorNotACommand -> {
|
is ParsedCommand.ErrorNotACommand -> {
|
||||||
// Send the text message to the room
|
// Send the text message to the room
|
||||||
|
if (state.rootThreadEventId != null)
|
||||||
|
room.replyInThread(state.rootThreadEventId, action.text.toString(), action.autoMarkdown)
|
||||||
|
else
|
||||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||||
|
|
||||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ data class TextComposerViewState(
|
||||||
val canSendMessage: Boolean = true,
|
val canSendMessage: Boolean = true,
|
||||||
val isVoiceRecording: Boolean = false,
|
val isVoiceRecording: Boolean = false,
|
||||||
val isSendButtonVisible: Boolean = false,
|
val isSendButtonVisible: Boolean = false,
|
||||||
|
val rootThreadEventId: String? = null,
|
||||||
val sendMode: SendMode = SendMode.REGULAR("", false)
|
val sendMode: SendMode = SendMode.REGULAR("", false)
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
|
@ -53,4 +54,6 @@ data class TextComposerViewState(
|
||||||
get() = canSendMessage && !isVoiceRecording
|
get() = canSendMessage && !isVoiceRecording
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
|
constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
|
||||||
|
|
||||||
|
fun isInThreadTimeline(): Boolean = rootThreadEventId != null
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,7 +149,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
// This is an edit event, we should display it when debugging as a notice event
|
// This is an edit event, we should display it when debugging as a notice event
|
||||||
return noticeItemFactory.create(params)
|
return noticeItemFactory.create(params)
|
||||||
}
|
}
|
||||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, event.root.isRootThread)
|
||||||
|
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
|
|
|
@ -31,7 +31,8 @@ class MessageItemAttributesFactory @Inject constructor(
|
||||||
|
|
||||||
fun create(messageContent: Any?,
|
fun create(messageContent: Any?,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
callback: TimelineEventController.Callback?): AbsMessageItem.Attributes {
|
callback: TimelineEventController.Callback?,
|
||||||
|
isRootThread: Boolean = false): AbsMessageItem.Attributes {
|
||||||
return AbsMessageItem.Attributes(
|
return AbsMessageItem.Attributes(
|
||||||
avatarSize = avatarSizeProvider.avatarSize,
|
avatarSize = avatarSizeProvider.avatarSize,
|
||||||
informationData = informationData,
|
informationData = informationData,
|
||||||
|
@ -49,7 +50,8 @@ class MessageItemAttributesFactory @Inject constructor(
|
||||||
reactionPillCallback = callback,
|
reactionPillCallback = callback,
|
||||||
avatarCallback = callback,
|
avatarCallback = callback,
|
||||||
readReceiptsCallback = callback,
|
readReceiptsCallback = callback,
|
||||||
emojiTypeFace = emojiCompatFontProvider.typeface
|
emojiTypeFace = emojiCompatFontProvider.typeface,
|
||||||
|
isRootThread = isRootThread
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||||
// Render send state indicator
|
// Render send state indicator
|
||||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||||
|
holder.isThread.isVisible = attributes.isRootThread
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: H) {
|
override fun unbind(holder: H) {
|
||||||
|
@ -117,6 +118,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
|
val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
|
||||||
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
|
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
|
||||||
|
val isThread by bind<View>(R.id.messageIsThread)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -133,7 +135,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||||
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
|
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
|
||||||
val avatarCallback: TimelineEventController.AvatarCallback? = null,
|
val avatarCallback: TimelineEventController.AvatarCallback? = null,
|
||||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||||
val emojiTypeFace: Typeface? = null
|
val emojiTypeFace: Typeface? = null,
|
||||||
|
val isRootThread: Boolean = false
|
||||||
) : AbsBaseMessageItem.Attributes {
|
) : AbsBaseMessageItem.Attributes {
|
||||||
|
|
||||||
// Have to override as it's used to diff epoxy items
|
// Have to override as it's used to diff epoxy items
|
||||||
|
|
|
@ -25,6 +25,8 @@ import im.vector.app.core.extensions.replaceFragment
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivityRoomThreadDetailBinding
|
import im.vector.app.databinding.ActivityRoomThreadDetailBinding
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
||||||
|
import im.vector.app.features.home.room.detail.RoomDetailFragment
|
||||||
import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs
|
import im.vector.app.features.home.room.threads.detail.arguments.RoomThreadDetailArgs
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -65,7 +67,16 @@ class RoomThreadDetailActivity : VectorBaseActivity<ActivityRoomThreadDetailBind
|
||||||
|
|
||||||
private fun initFragment() {
|
private fun initFragment() {
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
replaceFragment(R.id.roomThreadDetailFragmentContainer, RoomThreadDetailFragment::class.java, getRoomThreadDetailArgs(), FRAGMENT_TAG)
|
getRoomThreadDetailArgs()?.let {
|
||||||
|
replaceFragment(
|
||||||
|
R.id.roomThreadDetailFragmentContainer,
|
||||||
|
RoomDetailFragment::class.java,
|
||||||
|
RoomDetailArgs(
|
||||||
|
roomId = it.roomId,
|
||||||
|
roomThreadDetailArgs = it
|
||||||
|
))
|
||||||
|
}
|
||||||
|
// replaceFragment(R.id.roomThreadDetailFragmentContainer, RoomThreadDetailFragment::class.java, getRoomThreadDetailArgs(), FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,15 @@ class RoomThreadDetailFragment @Inject constructor(
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
initTextComposer()
|
initTextComposer()
|
||||||
// views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}"
|
// lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
// Realm.getInstance(realmConfiguration).executeTransaction {
|
||||||
|
// val eventId = roomThreadDetailArgs.eventId ?: return@executeTransaction
|
||||||
|
// val r = EventEntity.where(it, eventId = eventId)
|
||||||
|
// .findFirst() ?: return@executeTransaction
|
||||||
|
// Timber.i("------> $eventId isThread: ${EventMapper.map(r).isThread()}")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//// views.testTextVeiwddasda.text = "${roomThreadDetailArgs.eventId} -- ${roomThreadDetailArgs.roomId}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTextComposer(){
|
private fun initTextComposer(){
|
||||||
|
|
|
@ -200,4 +200,17 @@
|
||||||
</com.google.android.flexbox.FlexboxLayout>
|
</com.google.android.flexbox.FlexboxLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/messageIsThread"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:background="#2653AE"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:layout_below="@id/informationBottom"
|
||||||
|
android:layout_toStartOf="@id/messageSendStateImageView"
|
||||||
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
|
android:contentDescription="@string/room_threads_filter" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
Loading…
Reference in a new issue