mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +03:00
- Refactor thread awareness (handle decrypted rooms, images, stickers etc)
- Enable/disable threads functionality - New fallback thread implementation
This commit is contained in:
parent
e0630ceac0
commit
fe88e81d4a
24 changed files with 325 additions and 167 deletions
|
@ -201,18 +201,19 @@ data class Event(
|
|||
fun getDecryptedTextSummary(): String? {
|
||||
val text = getDecryptedValue() ?: return null
|
||||
return when {
|
||||
isReply() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
|
||||
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
|
||||
isFileMessage() -> "sent a file."
|
||||
isAudioMessage() -> "sent an audio file."
|
||||
isImageMessage() -> "sent an image."
|
||||
isVideoMessage() -> "sent a video."
|
||||
isSticker() -> "sent a sticker"
|
||||
isPoll() -> getPollQuestion() ?: "created a poll."
|
||||
else -> text
|
||||
}
|
||||
}
|
||||
|
||||
private fun Event.isQuote(): Boolean {
|
||||
if (isReply()) return false
|
||||
if (isReplyRenderedInThread()) return false
|
||||
return getDecryptedValue("formatted_body")?.contains("<blockquote>") ?: false
|
||||
}
|
||||
|
||||
|
@ -374,6 +375,10 @@ fun Event.isReply(): Boolean {
|
|||
return getRelationContent()?.inReplyTo?.eventId != null
|
||||
}
|
||||
|
||||
fun Event.isReplyRenderedInThread(): Boolean {
|
||||
return isReply() && getRelationContent()?.inReplyTo?.renderIn?.contains("m.thread") == true
|
||||
}
|
||||
|
||||
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
|
||||
|
||||
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
|
||||
|
|
|
@ -20,7 +20,6 @@ import io.realm.Realm
|
|||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
|
@ -40,7 +39,6 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
|
|||
roomId: String,
|
||||
realm: Realm, currentUserId: String,
|
||||
shouldUpdateNotifications: Boolean = true) {
|
||||
|
||||
for ((rootThreadEventId, eventEntity) in this) {
|
||||
eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let {
|
||||
if (it.isNullOrEmpty()) return@let
|
||||
|
@ -57,7 +55,7 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
|
|||
}
|
||||
}
|
||||
|
||||
if(shouldUpdateNotifications) {
|
||||
if (shouldUpdateNotifications) {
|
||||
updateNotificationsNew(roomId, realm, currentUserId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,9 +125,15 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
|
|||
return EventMapper.map(this, castJsonNumbers)
|
||||
}
|
||||
|
||||
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {
|
||||
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?, contentToInject: String? = null): EventEntity {
|
||||
return EventMapper.map(this, roomId).apply {
|
||||
this.sendState = sendState
|
||||
this.ageLocalTs = ageLocalTs
|
||||
contentToInject?.let {
|
||||
this.content = it
|
||||
if (this.type == EventType.STICKER) {
|
||||
this.type = EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import io.realm.RealmObject
|
|||
import io.realm.annotations.Index
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
@ -80,10 +79,10 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
|
||||
companion object
|
||||
|
||||
fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
|
||||
fun setDecryptionResult(result: MXEventDecryptionResult) {
|
||||
assertIsManaged()
|
||||
val decryptionResult = OlmDecryptionResult(
|
||||
payload = clearEvent ?: result.clearEvent,
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
|
|
|
@ -83,11 +83,9 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
|
||||
val threadList = response.chunks + listOfNotNull(response.originalEvent)
|
||||
|
||||
|
||||
return storeNewEventsIfNeeded(threadList, params.roomId)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Store new events if they are not already received, and returns weather or not,
|
||||
* a timeline update should be made
|
||||
|
@ -105,7 +103,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||
|
||||
for (event in threadList.reversed()) {
|
||||
|
||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
eventsSkipped++
|
||||
continue
|
||||
|
@ -180,7 +177,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
private fun handleReaction(realm: Realm,
|
||||
event: Event,
|
||||
roomId: String) {
|
||||
|
||||
val unsignedData = event.unsignedData ?: return
|
||||
val relatedEventId = event.eventId ?: return
|
||||
|
||||
|
@ -206,7 +202,6 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
sum.count += 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
|||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||
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.message.AudioInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.FileInfo
|
||||
|
@ -41,6 +42,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||
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.MessageVideoContent
|
||||
|
@ -293,7 +295,13 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
size = attachment.size
|
||||
),
|
||||
url = attachment.queryUri.toString(),
|
||||
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
eventId = it,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
)
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
@ -330,7 +338,13 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
thumbnailInfo = thumbnailInfo
|
||||
),
|
||||
url = attachment.queryUri.toString(),
|
||||
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
eventId = it,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
)
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
@ -354,7 +368,13 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
waveform = waveformSanitizer.sanitize(attachment.waveform)
|
||||
),
|
||||
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
|
||||
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
eventId = it,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
)
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
@ -368,7 +388,13 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
size = attachment.size
|
||||
),
|
||||
url = attachment.queryUri.toString(),
|
||||
relatesTo = rootThreadEventId?.let { RelationDefaultContent(RelationType.IO_THREAD, it) }
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
eventId = it,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
)
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
@ -378,6 +404,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
}
|
||||
|
||||
fun createEvent(roomId: String, type: String, content: Content?): Event {
|
||||
val newContent = enhanceStickerIfNeeded(type, content) ?: content
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
|
@ -385,11 +412,31 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = type,
|
||||
content = content,
|
||||
content = newContent,
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance sticker to support threads fallback if needed
|
||||
*/
|
||||
private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
|
||||
var newContent: Content? = null
|
||||
if (type == EventType.STICKER) {
|
||||
val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.IO_THREAD
|
||||
val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
|
||||
if (isThread && rootThreadEventId != null) {
|
||||
val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId))
|
||||
)
|
||||
newContent = (content.toModel<MessageStickerContent>())?.copy(
|
||||
relatesTo = newRelationalDefaultContent
|
||||
).toContent()
|
||||
}
|
||||
}
|
||||
return newContent
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a thread event related to the already existing root event
|
||||
*/
|
||||
|
@ -404,7 +451,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
return createEvent(
|
||||
roomId,
|
||||
EventType.MESSAGE,
|
||||
content.toThreadTextContent(rootThreadEventId, msgType)
|
||||
content.toThreadTextContent(
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
|
||||
msgType = msgType)
|
||||
.toContent())
|
||||
}
|
||||
|
||||
|
@ -471,8 +521,8 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
eventId = it,
|
||||
inReplyTo = ReplyToContent(eventId = eventId))
|
||||
} ?: RelationDefaultContent(null, null, ReplyToContent( eventId = eventId))
|
||||
inReplyTo = ReplyToContent(eventId = eventId, renderIn = arrayListOf("m.thread")))
|
||||
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
|
||||
|
||||
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
|
||||
return REPLY_PATTERN.format(
|
||||
|
@ -584,7 +634,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
roomId,
|
||||
markdownParser
|
||||
.parse(quoteText, force = true, advanced = autoMarkdown)
|
||||
.toThreadTextContent(rootThreadEventId, MessageType.MSGTYPE_TEXT)
|
||||
.toThreadTextContent(
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
|
||||
msgType = MessageType.MSGTYPE_TEXT)
|
||||
)
|
||||
} else {
|
||||
createFormattedTextEvent(
|
||||
|
@ -625,6 +678,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
// </mx-reply>
|
||||
// No whitespace because currently breaks temporary formatted text to Span
|
||||
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">In reply to</a> <a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
||||
const val QUOTE_PATTERN = """<blockquote><p>%s</p></blockquote><p>%s</p>"""
|
||||
|
||||
// This is used to replace inner mx-reply tags
|
||||
val MX_REPLY_REGEX = "<mx-reply>.*</mx-reply>".toRegex()
|
||||
|
|
|
@ -138,7 +138,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||
}
|
||||
}
|
||||
|
||||
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
|
||||
fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
||||
EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
|
||||
|
@ -215,4 +215,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest known thread event message, or the rootThreadEventId if no other event found
|
||||
*/
|
||||
fun getLatestThreadEvent(rootThreadEventId: String): String {
|
||||
return realmSessionProvider.withRealm { realm ->
|
||||
EventEntity.where(realm, eventId = rootThreadEventId).findFirst()?.threadSummaryLatestMessage?.eventId
|
||||
} ?: rootThreadEventId
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,12 +44,25 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT)
|
|||
)
|
||||
}
|
||||
|
||||
fun TextContent.toThreadTextContent(rootThreadEventId: String, msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
|
||||
/**
|
||||
* Transform a TextContent to a thread message content. It will also add the inReplyTo
|
||||
* latestThreadEventId in order for the clients without threads enabled to render it appropriately
|
||||
* If latest event not found, we pass rootThreadEventId
|
||||
*/
|
||||
fun TextContent.toThreadTextContent(
|
||||
rootThreadEventId: String,
|
||||
latestThreadEventId: String,
|
||||
msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
|
||||
return MessageTextContent(
|
||||
msgType = msgType,
|
||||
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
|
||||
body = text,
|
||||
relatesTo = RelationDefaultContent(type = RelationType.IO_THREAD, eventId = rootThreadEventId, inReplyTo = ReplyToContent(eventId = "CYIpEhDXkImqKD2TF9NSocxt4vU6hh98yXi5Ncusdaw")),
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
eventId = rootThreadEventId,
|
||||
inReplyTo = ReplyToContent(
|
||||
eventId = latestThreadEventId
|
||||
)),
|
||||
formattedBody = formattedText
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
|||
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
|
@ -297,9 +298,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
|
||||
if (timelineEvents.isEmpty()) return LoadedFromStorage()
|
||||
// Disabled due to the new fallback
|
||||
if(!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
fetchRootThreadEventsIfNeeded(timelineEvents)
|
||||
}
|
||||
// if(!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
// fetchRootThreadEventsIfNeeded(timelineEvents)
|
||||
// }
|
||||
if (direction == Timeline.Direction.FORWARDS) {
|
||||
builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
|
||||
}
|
||||
|
@ -353,6 +354,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
timelineEvent.root.mxDecryptionResult == null) {
|
||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
|
||||
}
|
||||
if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
// Thread aware for not encrypted events
|
||||
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
|
||||
}
|
||||
return timelineEvent
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
|
|||
import org.matrix.android.sdk.internal.crypto.NewSessionListener
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
|
@ -103,9 +104,27 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun threadAwareNonEncryptedEvents(request: DecryptionRequest, realm: Realm) {
|
||||
val event = request.event
|
||||
realm.executeTransaction {
|
||||
val eventId = event.eventId ?: return@executeTransaction
|
||||
val eventEntity = EventEntity
|
||||
.where(it, eventId = eventId)
|
||||
.findFirst()
|
||||
val decryptedEvent = eventEntity?.asDomain()
|
||||
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
|
||||
}
|
||||
}
|
||||
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
|
||||
val event = request.event
|
||||
val timelineId = request.timelineId
|
||||
|
||||
if (!request.event.isEncrypted()) {
|
||||
// Here we have requested a decryption to an event that is not encrypted
|
||||
// We will simply make this event thread aware
|
||||
threadAwareNonEncryptedEvents(request, realm)
|
||||
return
|
||||
}
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(request.event, timelineId)
|
||||
Timber.v("Successfully decrypted event ${event.eventId}")
|
||||
|
@ -114,21 +133,9 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||
val eventEntity = EventEntity
|
||||
.where(it, eventId = eventId)
|
||||
.findFirst()
|
||||
|
||||
eventEntity?.apply {
|
||||
val decryptedPayload =
|
||||
// Disabled due to the new fallback
|
||||
if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
threadsAwarenessHandler.handleIfNeededDuringDecryption(
|
||||
it,
|
||||
roomId = event.roomId,
|
||||
event,
|
||||
result)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
setDecryptionResult(result, decryptedPayload)
|
||||
}
|
||||
eventEntity?.setDecryptionResult(result)
|
||||
val decryptedEvent = eventEntity?.asDomain()
|
||||
threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
|
||||
}
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")
|
||||
|
|
|
@ -184,7 +184,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
}
|
||||
liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
|
||||
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
eventEntity.rootThreadEventId?.let {
|
||||
// This is a thread event
|
||||
optimizedThreadSummaryMap[it] = eventEntity
|
||||
|
@ -199,7 +199,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
|
||||
}
|
||||
|
||||
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,9 +104,9 @@ internal class SyncResponseHandler @Inject constructor(
|
|||
|
||||
// Prerequisite for thread events handling in RoomSyncHandler
|
||||
// Disabled due to the new fallback
|
||||
if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
|
||||
}
|
||||
// if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
// threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
|
||||
// }
|
||||
|
||||
// Start one big transaction
|
||||
monarchy.awaitTransaction { realm ->
|
||||
|
|
|
@ -381,16 +381,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
if (event.isEncrypted() && !isInitialSync) {
|
||||
decryptIfNeeded(event, roomId)
|
||||
}
|
||||
// Disabled due to the new fallback
|
||||
if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
threadsAwarenessHandler.handleIfNeeded(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
event = event)
|
||||
var contentToInject: String? = null
|
||||
if (!isInitialSync) {
|
||||
contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event)
|
||||
}
|
||||
|
||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType)
|
||||
if (event.stateKey != null) {
|
||||
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||
eventId = event.eventId
|
||||
|
@ -410,7 +407,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
}
|
||||
|
||||
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
eventEntity.rootThreadEventId?.let {
|
||||
// This is a thread event
|
||||
optimizedThreadSummaryMap[it] = eventEntity
|
||||
|
@ -447,7 +444,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
// Handle deletion of [stuck] local echos if needed
|
||||
deleteLocalEchosIfNeeded(insertType, roomEntity, eventList)
|
||||
|
||||
if(lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
|
||||
roomId = roomId,
|
||||
realm = realm,
|
||||
|
|
|
@ -18,11 +18,14 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
|
|||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.api.session.events.model.Content
|
||||
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.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.getRelationContentForType
|
||||
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
|
||||
import org.matrix.android.sdk.api.session.events.model.isSticker
|
||||
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.message.MessageFormat
|
||||
|
@ -32,20 +35,23 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
|||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
|
||||
import org.matrix.android.sdk.api.util.JsonDict
|
||||
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
|
||||
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
|
||||
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.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.query.copyToRealmOrIgnore
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -55,11 +61,16 @@ import javax.inject.Inject
|
|||
*/
|
||||
internal class ThreadsAwarenessHandler @Inject constructor(
|
||||
private val permalinkFactory: PermalinkFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val lightweightSettingsStorage: LightweightSettingsStorage,
|
||||
private val getEventTask: GetEventTask
|
||||
) {
|
||||
|
||||
// This caching is responsible to improve the performance when we receive a root event
|
||||
// to be able to know this event is a root one without checking the DB,
|
||||
// We update the list with all thread root events by checking if there is a m.thread relation on the events
|
||||
private val cacheEventRootId = hashSetOf<String>()
|
||||
|
||||
/**
|
||||
* Fetch root thread events if they are missing from the local storage
|
||||
* @param syncResponse the sync response
|
||||
|
@ -142,120 +153,186 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
|
||||
/**
|
||||
* Handle events mainly coming from the RoomSyncHandler
|
||||
* @return The content to inject in the roomSyncHandler live events
|
||||
*/
|
||||
fun handleIfNeeded(realm: Realm,
|
||||
roomId: String,
|
||||
event: Event) {
|
||||
val payload = transformThreadToReplyIfNeeded(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
event = event,
|
||||
decryptedResult = event.mxDecryptionResult?.payload) ?: return
|
||||
|
||||
event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle events while they are being decrypted
|
||||
*/
|
||||
fun handleIfNeededDuringDecryption(realm: Realm,
|
||||
roomId: String?,
|
||||
event: Event,
|
||||
result: MXEventDecryptionResult): JsonDict? {
|
||||
return transformThreadToReplyIfNeeded(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
event = event,
|
||||
decryptedResult = result.clearEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event is a thread event then transform/enhance it to a visual Reply Event,
|
||||
* If the event is not a thread event, null value will be returned
|
||||
* If there is an error (ex. the root/origin thread event is not found), null will be returned
|
||||
*/
|
||||
private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? {
|
||||
fun makeEventThreadAware(realm: Realm,
|
||||
roomId: String?,
|
||||
event: Event?,
|
||||
eventEntity: EventEntity? = null): String? {
|
||||
event ?: return null
|
||||
roomId ?: return null
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
|
||||
handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
|
||||
if (!isThreadEvent(event)) return null
|
||||
val rootThreadEventId = getRootThreadEventId(event) ?: return null
|
||||
val payload = decryptedResult?.toMutableMap() ?: return null
|
||||
var body = getValueFromPayload(payload, "body") ?: return null
|
||||
val msgType = getValueFromPayload(payload, "msgtype") ?: run {
|
||||
if (payload["type"]?.toString() == EventType.STICKER) {
|
||||
MessageType.MSGTYPE_STICKER_LOCAL
|
||||
} else {
|
||||
return null
|
||||
val eventPayload = if (!event.isEncrypted()) {
|
||||
event.content?.toMutableMap() ?: return null
|
||||
} else {
|
||||
event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
|
||||
}
|
||||
val eventBody = event.getDecryptedTextSummary() ?: return null
|
||||
val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
|
||||
return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
|
||||
}
|
||||
val eventToInject = getEventFromDB(realm, eventIdToInject)
|
||||
val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
|
||||
var contentForNonEncrypted: String?
|
||||
if (eventToInject != null && eventToInjectBody != null) {
|
||||
// If the event to inject exists and is decrypted
|
||||
// Inject it to our event
|
||||
val messageTextContent = injectEvent(
|
||||
roomId = roomId,
|
||||
eventBody = eventBody,
|
||||
eventToInject = eventToInject,
|
||||
eventToInjectBody = eventToInjectBody) ?: return null
|
||||
// update the event
|
||||
contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
|
||||
} else {
|
||||
contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
|
||||
}
|
||||
|
||||
// Now lets try to find relations for improved results, while some events may come with reverse order
|
||||
eventEntity?.let {
|
||||
// When eventEntity is not null means that we are not from within roomSyncHandler
|
||||
handleEventsThatRelatesTo(realm, roomId, event, eventBody, false)
|
||||
}
|
||||
return contentForNonEncrypted
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle for not thread events that we have marked them as root.
|
||||
* Find relations and inject them accordingly
|
||||
* @param eventEntity the current eventEntity received
|
||||
* @param event the current event received
|
||||
* @return The content to inject in the roomSyncHandler live events
|
||||
*/
|
||||
private fun handleRootThreadEventsIfNeeded(realm: Realm, roomId: String, eventEntity: EventEntity?, event: Event): String? {
|
||||
if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) {
|
||||
eventEntity?.let {
|
||||
val eventBody = event.getDecryptedTextSummary() ?: return null
|
||||
return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true)
|
||||
}
|
||||
}
|
||||
val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null
|
||||
val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null
|
||||
return null
|
||||
}
|
||||
|
||||
// Check the event type
|
||||
when (msgType) {
|
||||
MessageType.MSGTYPE_STICKER_LOCAL -> {
|
||||
body = "sent a sticker from within a thread"
|
||||
}
|
||||
MessageType.MSGTYPE_FILE -> {
|
||||
body = "sent a file from within a thread"
|
||||
}
|
||||
MessageType.MSGTYPE_VIDEO -> {
|
||||
body = "Sent a video from within a thread"
|
||||
}
|
||||
MessageType.MSGTYPE_IMAGE -> {
|
||||
body = "sent an image from within a thread"
|
||||
}
|
||||
MessageType.MSGTYPE_AUDIO -> {
|
||||
body = "sent an audio file from within a thread"
|
||||
}
|
||||
/**
|
||||
* This function is responsible to check if there is any event that relates to our current event
|
||||
* This is useful when we receive an event that relates to a missing parent, so when later we receive the parent
|
||||
* we can update the child as well
|
||||
* @param event the current event that we examine
|
||||
* @param eventBody the current body of the event
|
||||
* @param isFromCache determines whether or not we already know this is root thread event
|
||||
* @return The content to inject in the roomSyncHandler live events
|
||||
*/
|
||||
private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? {
|
||||
event.eventId ?: return null
|
||||
val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
|
||||
eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
|
||||
val newEventFound = eventEntityFound.asDomain()
|
||||
val newEventBody = newEventFound.getDecryptedTextSummary() ?: return null
|
||||
val newEventPayload = newEventFound.mxDecryptionResult?.payload?.toMutableMap() ?: return null
|
||||
|
||||
val messageTextContent = injectEvent(
|
||||
roomId = roomId,
|
||||
eventBody = newEventBody,
|
||||
eventToInject = event,
|
||||
eventToInjectBody = eventBody) ?: return null
|
||||
|
||||
return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
|
||||
}
|
||||
decryptIfNeeded(rootThreadEvent, roomId)
|
||||
return null
|
||||
}
|
||||
|
||||
val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body")
|
||||
/**
|
||||
* Actual update the eventEntity with the new payload
|
||||
* @return the content to inject when this is executed by RoomSyncHandler
|
||||
*/
|
||||
private fun updateEventEntity(event: Event,
|
||||
eventEntity: EventEntity?,
|
||||
eventPayload: MutableMap<String, Any>,
|
||||
messageTextContent: Content): String? {
|
||||
eventPayload["content"] = messageTextContent
|
||||
|
||||
val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false)
|
||||
val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: ""
|
||||
if (event.isEncrypted()) {
|
||||
if (event.isSticker()) {
|
||||
eventPayload["type"] = EventType.MESSAGE
|
||||
}
|
||||
event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = eventPayload)
|
||||
eventEntity?.decryptionResultJson = event.mxDecryptionResult?.let {
|
||||
MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(it)
|
||||
}
|
||||
} else {
|
||||
if (event.type == EventType.STICKER) {
|
||||
eventEntity?.type = EventType.MESSAGE
|
||||
}
|
||||
eventEntity?.content = ContentMapper.map(messageTextContent)
|
||||
return ContentMapper.map(messageTextContent)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Injecting $eventToInject decrypted content as a reply to $event
|
||||
* @param eventToInject the event that will inject
|
||||
* @param eventBody the actual event body
|
||||
* @return The final content with the injected event
|
||||
*/
|
||||
private fun injectEvent(roomId: String,
|
||||
eventBody: String,
|
||||
eventToInject: Event,
|
||||
eventToInjectBody: String): Content? {
|
||||
val eventToInjectId = eventToInject.eventId ?: return null
|
||||
val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
|
||||
val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
|
||||
val userLink = permalinkFactory.createPermalink(eventIdToInjectSenderId, false) ?: ""
|
||||
val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format(
|
||||
permalink,
|
||||
userLink,
|
||||
rootThreadEventSenderId,
|
||||
// Remove inner mx_reply tags if any
|
||||
rootThreadEventBody,
|
||||
body)
|
||||
eventIdToInjectSenderId,
|
||||
eventToInjectBody,
|
||||
eventBody)
|
||||
|
||||
return MessageTextContent(
|
||||
msgType = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageFormat.FORMAT_MATRIX_HTML,
|
||||
body = eventBody,
|
||||
formattedBody = replyFormatted
|
||||
).toContent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate fallback Quote reply
|
||||
*/
|
||||
private fun injectFallbackIndicator(event: Event,
|
||||
eventBody: String,
|
||||
eventEntity: EventEntity?,
|
||||
eventPayload: MutableMap<String, Any>): String? {
|
||||
val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
|
||||
"Replied within a thread",
|
||||
eventBody)
|
||||
|
||||
val messageTextContent = MessageTextContent(
|
||||
msgType = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageFormat.FORMAT_MATRIX_HTML,
|
||||
body = body,
|
||||
body = eventBody,
|
||||
formattedBody = replyFormatted
|
||||
).toContent()
|
||||
|
||||
payload["content"] = messageTextContent
|
||||
|
||||
return payload
|
||||
return updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the event
|
||||
*/
|
||||
|
||||
private fun decryptIfNeeded(event: Event, roomId: String) {
|
||||
try {
|
||||
if (!event.isEncrypted() || event.mxDecryptionResult != null) return
|
||||
|
||||
// Event from sync does not have roomId, so add it to the event first
|
||||
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
if (e is MXCryptoError.Base) {
|
||||
event.mCryptoError = e.errorType
|
||||
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
}
|
||||
private fun eventThatRelatesTo(realm: Realm, currentEventId: String, rootThreadEventId: String): List<EventEntity>? {
|
||||
val threadList = realm.where<EventEntity>()
|
||||
.beginGroup()
|
||||
.equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||
.or()
|
||||
.equalTo(EventEntityFields.EVENT_ID, rootThreadEventId)
|
||||
.endGroup()
|
||||
.and()
|
||||
.findAll()
|
||||
cacheEventRootId.add(rootThreadEventId)
|
||||
return threadList.filter {
|
||||
it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,9 +358,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
*/
|
||||
private fun getRootThreadEventId(event: Event): String? =
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
// private fun getRootThreadEventId(event: Event): String? =
|
||||
// event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId ?:
|
||||
// event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
|
||||
private fun getPreviousEventOrRoot(event: Event): String? =
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
|
||||
|
|
|
@ -83,7 +83,6 @@ import im.vector.app.features.themes.ThemeUtils
|
|||
import im.vector.app.receivers.DebugReceiver
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.matrix.android.sdk.api.Matrix
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.GlobalError
|
||||
import reactivecircus.flowbinding.android.view.clicks
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.features.autocomplete.AutocompleteClickListener
|
||||
import im.vector.app.features.autocomplete.RecyclerViewPresenter
|
||||
import im.vector.app.features.command.Command
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.features.command
|
||||
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.isMsisdn
|
||||
import im.vector.app.features.home.room.detail.ChatEffect
|
||||
|
|
|
@ -68,7 +68,6 @@ import com.airbnb.mvrx.fragmentViewModel
|
|||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.vanniktech.emoji.EmojiPopup
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||
|
|
|
@ -282,6 +282,7 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
copy(myRoomMember = it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPreviewUrlObservers() {
|
||||
if (!vectorPreferences.showUrlPreviews()) {
|
||||
return
|
||||
|
@ -488,6 +489,7 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
val content = initialState.rootThreadEventId?.let {
|
||||
action.stickerContent.copy(relatesTo = RelationDefaultContent(RelationType.IO_THREAD, it))
|
||||
} ?: action.stickerContent
|
||||
|
||||
room.sendEvent(EventType.STICKER, content.toContent())
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
|||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
|
|
|
@ -20,7 +20,6 @@ import dagger.Lazy
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
|||
|
||||
import im.vector.app.core.extensions.localDateTime
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||
|
|
|
@ -29,7 +29,6 @@ import androidx.core.view.isInvisible
|
|||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
|
|
|
@ -48,6 +48,5 @@ class VectorSettingsLabsFragment @Inject constructor(
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue