mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +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
|
||||
var sendStateDetails: String? = null
|
||||
|
||||
@Transient
|
||||
var isRootThread: Boolean = false
|
||||
|
||||
fun sendStateError(): MatrixError? {
|
||||
return sendStateDetails?.let {
|
||||
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
||||
|
@ -241,54 +244,54 @@ data class Event(
|
|||
fun Event.isTextMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE &&
|
||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_NOTICE -> true
|
||||
else -> false
|
||||
}
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_NOTICE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isImageMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE &&
|
||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||
MessageType.MSGTYPE_IMAGE -> true
|
||||
else -> false
|
||||
}
|
||||
MessageType.MSGTYPE_IMAGE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isVideoMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE &&
|
||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||
MessageType.MSGTYPE_VIDEO -> true
|
||||
else -> false
|
||||
}
|
||||
MessageType.MSGTYPE_VIDEO -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isAudioMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE &&
|
||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||
MessageType.MSGTYPE_AUDIO -> true
|
||||
else -> false
|
||||
}
|
||||
MessageType.MSGTYPE_AUDIO -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isFileMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE &&
|
||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isAttachmentMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE &&
|
||||
when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.getRelationContent(): RelationDefaultContent? {
|
||||
|
@ -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 {
|
||||
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 {
|
||||
return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
|
||||
return getRelationContentForType(RelationType.REPLACE)?.eventId != null
|
||||
}
|
||||
|
||||
fun Event.getPresenceContent(): PresenceContent? {
|
||||
|
|
|
@ -131,11 +131,11 @@ interface RelationService {
|
|||
* Creates a thread reply for an existing timeline event
|
||||
* The replyInThreadText can be a Spannable and contains special spans (MatrixItemSpan) that will be translated
|
||||
* 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 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,
|
||||
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.create.RoomCreateContent
|
||||
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.EditAggregatedSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
|
||||
|
@ -49,7 +50,7 @@ import timber.log.Timber
|
|||
|
||||
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) {
|
||||
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
|
||||
|
@ -72,6 +73,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
|
|||
if (oldVersion <= 15) migrateTo16(realm)
|
||||
if (oldVersion <= 16) migrateTo17(realm)
|
||||
if (oldVersion <= 17) migrateTo18(realm)
|
||||
if (oldVersion <= 18) migrateTo19(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -364,4 +366,13 @@ internal object RealmSessionStoreMigration : RealmMigration {
|
|||
realm.schema.get("RoomMemberSummaryEntity")
|
||||
?.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.EventType
|
||||
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.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
|
@ -39,6 +41,8 @@ internal object EventMapper {
|
|||
eventEntity.isUseless = IsUselessResolver.isUseless(event)
|
||||
eventEntity.stateKey = event.stateKey
|
||||
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.originServerTs = event.originServerTs
|
||||
eventEntity.redacts = event.redacts
|
||||
|
@ -93,6 +97,7 @@ internal object EventMapper {
|
|||
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||
}
|
||||
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,
|
||||
// Because of gaps we can have several chunks with nextToken == null
|
||||
@Index var nextToken: String? = null,
|
||||
@Index var rootThreadEventId: String? = null,
|
||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
var numberOfTimelineEvents: Long = 0,
|
||||
|
@ -44,6 +45,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
|
|||
val room: RealmResults<RoomEntity>? = null
|
||||
|
||||
companion object
|
||||
|
||||
fun isThreadChunk() = rootThreadEventId != null
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
|
||||
|
|
|
@ -27,13 +27,15 @@ import org.matrix.android.sdk.internal.extensions.assertIsManaged
|
|||
internal open class EventEntity(@Index var eventId: String = "",
|
||||
@Index var roomId: String = "",
|
||||
@Index var type: String = "",
|
||||
@Index var isThread: Boolean = false,
|
||||
var rootThreadEventId: String? = null,
|
||||
var content: String? = null,
|
||||
var prevContent: String? = null,
|
||||
var isUseless: Boolean = false,
|
||||
@Index var stateKey: String? = null,
|
||||
var originServerTs: Long? = null,
|
||||
@Index var sender: String? = null,
|
||||
// Can contain a serialized MatrixError
|
||||
// Can contain a serialized MatrixError
|
||||
var sendStateDetails: String? = null,
|
||||
var age: Long? = 0,
|
||||
var unsignedData: String? = null,
|
||||
|
@ -75,4 +77,10 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
.findFirst()
|
||||
?.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)
|
||||
if (prevToken != null) {
|
||||
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
|
||||
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
}
|
||||
if (nextToken != null) {
|
||||
query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken)
|
||||
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
}
|
||||
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? {
|
||||
return where(realm, roomId)
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
|
||||
internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
|
||||
return realm.where<ChunkEntity>()
|
||||
.`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
|
||||
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
.findAll()
|
||||
}
|
||||
|
||||
|
@ -56,6 +61,7 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
|
|||
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
|
||||
}
|
||||
|
||||
|
||||
internal fun ChunkEntity.Companion.create(
|
||||
realm: Realm,
|
||||
prevToken: String?,
|
||||
|
@ -66,3 +72,16 @@ internal fun ChunkEntity.Companion.create(
|
|||
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? {
|
||||
val event = eventFactory.createThreadTextEvent(eventToReplyInThread, TextContent(replyInThreadText.toString()))
|
||||
.also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
override fun replyInThread(rootThreadEventId: String, replyInThreadText: CharSequence, autoMarkdown: Boolean): Cancelable {
|
||||
val event = eventFactory.createThreadTextEvent(
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
roomId = roomId,
|
||||
text = replyInThreadText.toString(),
|
||||
autoMarkdown = autoMarkdown)
|
||||
// .also {
|
||||
// saveLocalEcho(it)
|
||||
// }
|
||||
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.RelationDefaultContent
|
||||
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.getLastMessageContent
|
||||
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.permalinks.PermalinkFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -346,11 +343,13 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
/**
|
||||
* 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(
|
||||
eventToReplyInThread.roomId,
|
||||
roomId,
|
||||
EventType.MESSAGE,
|
||||
textContent.toThreadTextContent(eventToReplyInThread).toContent())
|
||||
createTextContent(text, autoMarkdown)
|
||||
.toThreadTextContent(rootThreadEventId)
|
||||
.toContent())
|
||||
|
||||
private fun dummyOriginServerTs(): Long {
|
||||
return System.currentTimeMillis()
|
||||
|
|
|
@ -16,13 +16,11 @@
|
|||
|
||||
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.MessageTextContent
|
||||
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.threads.ThreadRelatesTo
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
|
||||
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
||||
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 {
|
||||
return ThreadTextContent(
|
||||
fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
|
||||
return MessageTextContent(
|
||||
msgType = msgType,
|
||||
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
|
||||
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 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.isThread
|
||||
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.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.mapper.toEntity
|
||||
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.RoomEntity
|
||||
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.findAllIncludingEvents
|
||||
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.where
|
||||
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>()
|
||||
}
|
||||
|
||||
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
|
||||
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
||||
|
@ -247,4 +260,43 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
|||
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.events.model.Event
|
||||
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.initsync.InitSyncStep
|
||||
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.message.MessageRelationContent
|
||||
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.LazyRoomSyncEphemeral
|
||||
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.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.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
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.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.toEntity
|
||||
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.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.RoomEntity
|
||||
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.query.copyToRealmOrIgnore
|
||||
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.findThreadChunkOfRoom
|
||||
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.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.room.membership.RoomChangeMembershipStateDataSource
|
||||
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.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.typing.TypingEventContent
|
||||
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 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) {
|
||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
continue
|
||||
|
@ -385,7 +413,16 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
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
|
||||
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
|
||||
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
||||
|
||||
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) {
|
||||
try {
|
||||
// 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.view.ViewCompat
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
|
@ -228,7 +229,8 @@ data class RoomDetailArgs(
|
|||
val roomId: String,
|
||||
val eventId: String? = null,
|
||||
val sharedData: SharedData? = null,
|
||||
val openShareSpaceForId: String? = null
|
||||
val openShareSpaceForId: String? = null,
|
||||
val roomThreadDetailArgs: RoomThreadDetailArgs? = null
|
||||
) : Parcelable
|
||||
|
||||
class RoomDetailFragment @Inject constructor(
|
||||
|
@ -352,7 +354,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
)
|
||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||
lazyLoadedViews.bind(views)
|
||||
setupToolbar(views.roomToolbar)
|
||||
if (isThreadTimeLine()) {
|
||||
views.roomToolbar.isGone = true
|
||||
} else {
|
||||
setupToolbar(views.roomToolbar)
|
||||
}
|
||||
setupThreadIfNeeded()
|
||||
setupRecyclerView()
|
||||
setupComposer()
|
||||
setupNotificationView()
|
||||
|
@ -390,10 +397,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
return@onEach
|
||||
}
|
||||
when (mode) {
|
||||
is SendMode.REGULAR -> renderRegularMode(mode.text)
|
||||
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
|
||||
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
|
||||
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
||||
is SendMode.REGULAR -> renderRegularMode(mode.text)
|
||||
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
|
||||
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
|
||||
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -902,6 +909,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
if (isThreadTimeLine()) return
|
||||
// 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 ->
|
||||
menuItem.actionView.setOnClickListener {
|
||||
|
@ -915,6 +923,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
if (isThreadTimeLine()) {
|
||||
menu.forEach {
|
||||
it.isVisible = false
|
||||
}
|
||||
return
|
||||
}
|
||||
menu.forEach {
|
||||
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
|
||||
}
|
||||
|
@ -1180,6 +1194,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun setupThreadIfNeeded(){
|
||||
getRootThreadEventId()?.let{
|
||||
textComposerViewModel.handle(TextComposerAction.EnterReplyInThreadTimeline(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
timelineEventController.callback = this
|
||||
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,8 +65,9 @@ data class RoomDetailViewState(
|
|||
val isAllowedToManageWidgets: Boolean = false,
|
||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||
val hasFailedSending: Boolean = false,
|
||||
val jitsiState: JitsiState = JitsiState()
|
||||
) : MavericksState {
|
||||
val jitsiState: JitsiState = JitsiState(),
|
||||
val rootThreadEventId: String? = null
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(
|
||||
roomId = args.roomId,
|
||||
|
|
|
@ -28,4 +28,6 @@ sealed class TextComposerAction : VectorViewModelAction {
|
|||
data class UserIsTyping(val isTyping: Boolean) : TextComposerAction()
|
||||
data class OnTextChanged(val text: CharSequence) : 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.session.Session
|
||||
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.toModel
|
||||
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.OnTextChanged -> handleOnTextChanged(action)
|
||||
is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action)
|
||||
is TextComposerAction.EnterReplyInThreadTimeline -> handleEnterReplyInThreadTimeline(action)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,6 +98,10 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||
copy(isVoiceRecording = action.isRecording)
|
||||
}
|
||||
|
||||
private fun handleEnterReplyInThreadTimeline(action: TextComposerAction.EnterReplyInThreadTimeline) = setState {
|
||||
copy(rootThreadEventId = action.rootThreadEventId)
|
||||
}
|
||||
|
||||
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) {
|
||||
setState {
|
||||
// Makes sure currentComposerText is upToDate when accessing further setState
|
||||
|
@ -151,11 +158,15 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||
private fun handleSendMessage(action: TextComposerAction.SendMessage) {
|
||||
withState { state ->
|
||||
when (state.sendMode) {
|
||||
is SendMode.REGULAR -> {
|
||||
is SendMode.REGULAR -> {
|
||||
when (val slashCommandResult = CommandParser.parseSplashCommand(action.text)) {
|
||||
is ParsedCommand.ErrorNotACommand -> {
|
||||
// Send the text message to the room
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
if (state.rootThreadEventId != null)
|
||||
room.replyInThread(state.rootThreadEventId, action.text.toString(), action.autoMarkdown)
|
||||
else
|
||||
room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||
|
||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
|
@ -386,7 +397,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||
}
|
||||
}.exhaustive
|
||||
}
|
||||
is SendMode.EDIT -> {
|
||||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||
if (inReplyTo != null) {
|
||||
|
@ -409,7 +420,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.QUOTE -> {
|
||||
is SendMode.QUOTE -> {
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
|
@ -430,7 +441,7 @@ class TextComposerViewModel @AssistedInject constructor(
|
|||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.REPLY -> {
|
||||
is SendMode.REPLY -> {
|
||||
state.sendMode.timelineEvent.let {
|
||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||
_viewEvents.post(TextComposerViewEvents.MessageSent)
|
||||
|
|
|
@ -46,6 +46,7 @@ data class TextComposerViewState(
|
|||
val canSendMessage: Boolean = true,
|
||||
val isVoiceRecording: Boolean = false,
|
||||
val isSendButtonVisible: Boolean = false,
|
||||
val rootThreadEventId: String? = null,
|
||||
val sendMode: SendMode = SendMode.REGULAR("", false)
|
||||
) : MavericksState {
|
||||
|
||||
|
@ -53,4 +54,6 @@ data class TextComposerViewState(
|
|||
get() = canSendMessage && !isVoiceRecording
|
||||
|
||||
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
|
||||
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 ev = all.toModel<Event>()
|
||||
|
|
|
@ -31,7 +31,8 @@ class MessageItemAttributesFactory @Inject constructor(
|
|||
|
||||
fun create(messageContent: Any?,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): AbsMessageItem.Attributes {
|
||||
callback: TimelineEventController.Callback?,
|
||||
isRootThread: Boolean = false): AbsMessageItem.Attributes {
|
||||
return AbsMessageItem.Attributes(
|
||||
avatarSize = avatarSizeProvider.avatarSize,
|
||||
informationData = informationData,
|
||||
|
@ -49,7 +50,8 @@ class MessageItemAttributesFactory @Inject constructor(
|
|||
reactionPillCallback = callback,
|
||||
avatarCallback = 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
|
||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||
holder.isThread.isVisible = attributes.isRootThread
|
||||
}
|
||||
|
||||
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 sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
|
||||
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,
|
||||
val avatarCallback: TimelineEventController.AvatarCallback? = null,
|
||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||
val emojiTypeFace: Typeface? = null
|
||||
val emojiTypeFace: Typeface? = null,
|
||||
val isRootThread: Boolean = false
|
||||
) : AbsBaseMessageItem.Attributes {
|
||||
|
||||
// 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.databinding.ActivityRoomThreadDetailBinding
|
||||
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 org.matrix.android.sdk.api.util.MatrixItem
|
||||
import javax.inject.Inject
|
||||
|
@ -65,7 +67,16 @@ class RoomThreadDetailActivity : VectorBaseActivity<ActivityRoomThreadDetailBind
|
|||
|
||||
private fun initFragment() {
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
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(){
|
||||
|
|
|
@ -200,4 +200,17 @@
|
|||
</com.google.android.flexbox.FlexboxLayout>
|
||||
</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>
|
Loading…
Reference in a new issue