mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 12:18:48 +03:00
Merge remote-tracking branch 'origin/develop' into bugfix/eric/voting-ended-poll
# Conflicts: # vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt # vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt # vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt
This commit is contained in:
commit
9806f1bf8f
165 changed files with 3393 additions and 682 deletions
1
changelog.d/4533.misc
Normal file
1
changelog.d/4533.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Improve headers UI in Rooms/Messages lists
|
1
changelog.d/5230.feature
Normal file
1
changelog.d/5230.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Thread timeline is now live and much faster especially for large or old threads
|
1
changelog.d/5232.feature
Normal file
1
changelog.d/5232.feature
Normal file
|
@ -0,0 +1 @@
|
|||
View all threads per room screen is now live when the home server supports threads
|
1
changelog.d/5271.sdk
Normal file
1
changelog.d/5271.sdk
Normal file
|
@ -0,0 +1 @@
|
|||
Adds support for MSC3440, additional threads homeserver capabilities
|
1
changelog.d/5340.bugfix
Normal file
1
changelog.d/5340.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Support both stable and unstable prefixes for Events about Polls and Location
|
1
changelog.d/5375.wip
Normal file
1
changelog.d/5375.wip
Normal file
|
@ -0,0 +1 @@
|
|||
Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities
|
1
changelog.d/5514.bugfix
Normal file
1
changelog.d/5514.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Read receipt in wrong order
|
1
changelog.d/5522.feature
Normal file
1
changelog.d/5522.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Poll Integration Tests
|
|
@ -58,6 +58,7 @@ ext.libs = [
|
|||
'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle",
|
||||
'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
|
||||
'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle",
|
||||
'lifecycleRuntimeKtx' : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle",
|
||||
'datastore' : "androidx.datastore:datastore:1.0.0",
|
||||
'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0",
|
||||
'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2",
|
||||
|
@ -141,4 +142,4 @@ ext.libs = [
|
|||
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
||||
'junit' : "junit:junit:4.13.2"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
|
||||
import org.matrix.android.sdk.api.session.room.send.UserDraft
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
|
@ -101,13 +102,18 @@ class FlowRoom(private val room: Room) {
|
|||
return room.getLiveRoomNotificationState().asFlow()
|
||||
}
|
||||
|
||||
fun liveThreadSummaries(): Flow<List<ThreadSummary>> {
|
||||
return room.getAllThreadSummariesLive().asFlow()
|
||||
.startWith(room.coroutineDispatchers.io) {
|
||||
room.getAllThreadSummaries()
|
||||
}
|
||||
}
|
||||
fun liveThreadList(): Flow<List<ThreadRootEvent>> {
|
||||
return room.getAllThreadsLive().asFlow()
|
||||
.startWith(room.coroutineDispatchers.io) {
|
||||
room.getAllThreads()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> {
|
||||
return room.getMarkedThreadNotificationsLive().asFlow()
|
||||
.startWith(room.coroutineDispatchers.io) {
|
||||
|
|
|
@ -62,7 +62,11 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealm(it)
|
||||
}
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
chunk.addTimelineEvent(
|
||||
roomId = ROOM_ID,
|
||||
eventEntity = fakeEvent,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
chunk.timelineEvents.size shouldBeEqualTo 1
|
||||
}
|
||||
}
|
||||
|
@ -74,8 +78,16 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealm(it)
|
||||
}
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
chunk.addTimelineEvent(
|
||||
roomId = ROOM_ID,
|
||||
eventEntity = fakeEvent,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
chunk.addTimelineEvent(
|
||||
roomId = ROOM_ID,
|
||||
eventEntity = fakeEvent,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
chunk.timelineEvents.size shouldBeEqualTo 1
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +156,11 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||
val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealm(it)
|
||||
}
|
||||
addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
|
||||
addTimelineEvent(
|
||||
roomId = roomId,
|
||||
eventEntity = fakeEvent,
|
||||
direction = direction,
|
||||
roomMemberContentsByUser = emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright 2020 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.session.room.timeline
|
||||
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeGreaterThan
|
||||
import org.amshove.kluent.shouldContain
|
||||
import org.amshove.kluent.shouldContainAll
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import org.matrix.android.sdk.InstrumentedTest
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.common.CommonTestHelper
|
||||
import org.matrix.android.sdk.common.CryptoTestHelper
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class PollAggregationTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun testAllPollUseCases() {
|
||||
val commonTestHelper = CommonTestHelper(context())
|
||||
val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val roomFromBobPOV = cryptoTestData.secondSession!!.getRoom(cryptoTestData.roomId)!!
|
||||
// Bob creates a poll
|
||||
roomFromBobPOV.sendPoll(PollType.DISCLOSED, pollQuestion, pollOptions)
|
||||
|
||||
aliceSession.startSync(true)
|
||||
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
|
||||
aliceTimeline.start()
|
||||
|
||||
val TOTAL_TEST_COUNT = 7
|
||||
val lock = CountDownLatch(TOTAL_TEST_COUNT)
|
||||
|
||||
val aliceEventsListener = object : Timeline.Listener {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
|
||||
val pollEventId = pollEvent.eventId
|
||||
val pollContent = pollEvent.root.content?.toModel<MessagePollContent>()
|
||||
val pollSummary = pollEvent.annotations?.pollResponseSummary
|
||||
|
||||
if (pollContent == null) {
|
||||
fail("Poll content is null")
|
||||
return
|
||||
}
|
||||
|
||||
when (lock.count.toInt()) {
|
||||
TOTAL_TEST_COUNT -> {
|
||||
// Poll has just been created.
|
||||
testInitialPollConditions(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 1 -> {
|
||||
// Bob: Option 1
|
||||
testBobVotesOption1(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 2 -> {
|
||||
// Bob: Option 2
|
||||
testBobChangesVoteToOption2(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 3 -> {
|
||||
// Alice: Option 2, Bob: Option 2
|
||||
testAliceAndBobVoteToOption2(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 4 -> {
|
||||
// Alice: Option 1, Bob: Option 2
|
||||
testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
|
||||
lock.countDown()
|
||||
roomFromBobPOV.endPoll(pollEventId)
|
||||
}
|
||||
TOTAL_TEST_COUNT - 5 -> {
|
||||
// Alice: Option 1, Bob: Option 2 [poll is ended]
|
||||
testEndedPoll(pollSummary)
|
||||
lock.countDown()
|
||||
roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
|
||||
}
|
||||
TOTAL_TEST_COUNT - 6 -> {
|
||||
// Alice: Option 1 (ignore change), Bob: Option 2 [poll is ended]
|
||||
testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
|
||||
testEndedPoll(pollSummary)
|
||||
lock.countDown()
|
||||
}
|
||||
else -> {
|
||||
fail("Lock count ${lock.count} didn't handled.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceSession.stopSync()
|
||||
aliceTimeline.dispose()
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
|
||||
private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
// No votes yet, poll summary should be null
|
||||
pollSummary shouldBe null
|
||||
// Question should be the same as intended
|
||||
pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() shouldBeEqualTo pollQuestion
|
||||
// Options should be the same as intended
|
||||
pollContent.getBestPollCreationInfo()?.answers?.let { answers ->
|
||||
answers.size shouldBeEqualTo pollOptions.size
|
||||
answers.map { it.getBestAnswer() } shouldContainAll pollOptions
|
||||
}
|
||||
}
|
||||
|
||||
private fun testBobVotesOption1(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val answerId = pollContent.getBestPollCreationInfo()?.answers?.first()?.id
|
||||
// Check if the intended vote is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 1)
|
||||
aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testBobChangesVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
|
||||
// Check if the intended vote is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 1)
|
||||
aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testAliceAndBobVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
|
||||
// Check if the intended votes is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 2)
|
||||
aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votes?.get(1)?.option shouldBeEqualTo answerId
|
||||
aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 2
|
||||
aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testAliceVotesOption1AndBobVotesOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
|
||||
if (pollSummary == null) {
|
||||
fail("Poll summary shouldn't be null when someone votes")
|
||||
return
|
||||
}
|
||||
val firstAnswerId = pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id
|
||||
val secondAnswerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
|
||||
// Check if the intended votes is in poll summary
|
||||
pollSummary.aggregatedContent?.let { aggregatedContent ->
|
||||
assertTotalVotesCount(aggregatedContent, 2)
|
||||
aggregatedContent.votes!!.map { it.option } shouldContain firstAnswerId
|
||||
aggregatedContent.votes!!.map { it.option } shouldContain secondAnswerId
|
||||
aggregatedContent.votesSummary?.get(firstAnswerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(secondAnswerId)?.total shouldBeEqualTo 1
|
||||
aggregatedContent.votesSummary?.get(firstAnswerId)?.percentage shouldBeEqualTo 0.5
|
||||
aggregatedContent.votesSummary?.get(secondAnswerId)?.percentage shouldBeEqualTo 0.5
|
||||
} ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
|
||||
}
|
||||
|
||||
private fun testEndedPoll(pollSummary: PollResponseAggregatedSummary?) {
|
||||
pollSummary?.closedTime ?: 0 shouldBeGreaterThan 0
|
||||
}
|
||||
|
||||
private fun assertTotalVotesCount(aggregatedContent: PollSummaryContent, expectedVoteCount: Int) {
|
||||
aggregatedContent.totalVotes shouldBeEqualTo expectedVoteCount
|
||||
aggregatedContent.votes?.size shouldBeEqualTo expectedVoteCount
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val pollQuestion = "Do you like creating polls?"
|
||||
val pollOptions = listOf("Yes", "Absolutely", "As long as tests pass")
|
||||
}
|
||||
}
|
|
@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class AggregatedRelations(
|
||||
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
|
||||
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
|
||||
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
|
||||
@Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
|
||||
)
|
||||
|
|
|
@ -201,7 +201,11 @@ data class Event(
|
|||
*/
|
||||
fun getDecryptedTextSummary(): String? {
|
||||
if (isRedacted()) return "Message Deleted"
|
||||
val text = getDecryptedValue() ?: return null
|
||||
val text = getDecryptedValue() ?: run {
|
||||
if (isPoll()) { return getPollQuestion() ?: "created a poll." }
|
||||
return null
|
||||
}
|
||||
|
||||
return when {
|
||||
isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
|
||||
isFileMessage() -> "sent a file."
|
||||
|
@ -349,7 +353,7 @@ fun Event.isAttachmentMessage(): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
|
||||
fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
|
||||
|
||||
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
|
||||
|
||||
|
@ -372,7 +376,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
|
|||
* Returns the poll question or null otherwise
|
||||
*/
|
||||
fun Event.getPollQuestion(): String? =
|
||||
getPollContent()?.pollCreationInfo?.question?.question
|
||||
getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
|
||||
|
||||
/**
|
||||
* Returns the relation content for a specific type or null otherwise
|
||||
|
@ -385,12 +389,12 @@ fun Event.isReply(): Boolean {
|
|||
}
|
||||
|
||||
fun Event.isReplyRenderedInThread(): Boolean {
|
||||
return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
|
||||
return isReply() && getRelationContent()?.shouldRenderInThread() == true
|
||||
}
|
||||
|
||||
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
|
||||
fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
|
||||
|
||||
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
|
||||
fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
|
||||
|
||||
fun Event.isEdition(): Boolean {
|
||||
return getRelationContentForType(RelationType.REPLACE)?.eventId != null
|
||||
|
|
|
@ -103,9 +103,9 @@ object EventType {
|
|||
const val REACTION = "m.reaction"
|
||||
|
||||
// Poll
|
||||
const val POLL_START = "org.matrix.msc3381.poll.start"
|
||||
const val POLL_RESPONSE = "org.matrix.msc3381.poll.response"
|
||||
const val POLL_END = "org.matrix.msc3381.poll.end"
|
||||
val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
|
||||
val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
|
||||
val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
|
||||
|
||||
// Unwedging
|
||||
internal const val DUMMY = "m.dummy"
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2022 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.events.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LatestThreadUnsignedRelation(
|
||||
override val limited: Boolean? = false,
|
||||
override val count: Int? = 0,
|
||||
@Json(name = "latest_event")
|
||||
val event: Event? = null,
|
||||
@Json(name = "current_user_participated")
|
||||
val isUserParticipating: Boolean? = false
|
||||
|
||||
) : UnsignedRelationInfo
|
|
@ -30,7 +30,6 @@ object RelationType {
|
|||
|
||||
/** Lets you define an event which is a thread reply to an existing event.*/
|
||||
const val THREAD = "m.thread"
|
||||
const val IO_THREAD = "io.element.thread"
|
||||
|
||||
/** Lets you define an event which adds a response to an existing event.*/
|
||||
const val RESPONSE = "org.matrix.response"
|
||||
|
|
|
@ -50,7 +50,11 @@ data class HomeServerCapabilities(
|
|||
* This capability describes the default and available room versions a server supports, and at what level of stability.
|
||||
* Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
|
||||
*/
|
||||
val roomVersions: RoomVersionCapabilities? = null
|
||||
val roomVersions: RoomVersionCapabilities? = null,
|
||||
/**
|
||||
* True if the home server support threading
|
||||
*/
|
||||
var canUseThreading: Boolean = false
|
||||
) {
|
||||
|
||||
enum class RoomCapabilitySupport {
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
|
|||
import org.matrix.android.sdk.api.session.room.state.StateService
|
||||
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
||||
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
|
||||
import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
||||
|
@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional
|
|||
interface Room :
|
||||
TimelineService,
|
||||
ThreadsService,
|
||||
ThreadsLocalService,
|
||||
SendService,
|
||||
DraftService,
|
||||
ReadService,
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
|
@ -216,6 +217,11 @@ interface RoomService {
|
|||
pagedListConfig: PagedList.Config = defaultPagedListConfig,
|
||||
sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
|
||||
|
||||
/**
|
||||
* Retrieve a flow on the number of rooms.
|
||||
*/
|
||||
fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int>
|
||||
|
||||
/**
|
||||
* TODO Doc
|
||||
*/
|
||||
|
|
|
@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
|||
|
||||
interface UpdatableLivePageResult {
|
||||
val livePagedList: LiveData<PagedList<RoomSummary>>
|
||||
|
||||
fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
|
||||
|
||||
val liveBoundaries: LiveData<ResultBoundaries>
|
||||
var queryParams: RoomSummaryQueryParams
|
||||
}
|
||||
|
||||
data class ResultBoundaries(
|
||||
|
|
|
@ -39,37 +39,46 @@ data class MessageLocationContent(
|
|||
*/
|
||||
@Json(name = "geo_uri") val geoUri: String,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
/**
|
||||
* See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
|
||||
@Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
|
||||
@Json(name = "m.location") val locationInfo: LocationInfo? = null,
|
||||
/**
|
||||
* Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null,
|
||||
@Json(name = "m.ts") val ts: Long? = null,
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
|
||||
@Json(name = "m.text") val text: String? = null,
|
||||
/**
|
||||
* m.asset defines a generic asset that can be used for location tracking but also in other places like
|
||||
* inventories, geofencing, checkins/checkouts etc.
|
||||
* It should contain a mandatory namespaced type key defining what particular asset is being referred to.
|
||||
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
|
||||
*/
|
||||
@Json(name = "m.asset") val locationAsset: LocationAsset? = null,
|
||||
|
||||
/**
|
||||
* Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
|
||||
|
||||
@Json(name = "org.matrix.msc1767.text") val text: String? = null
|
||||
@Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
|
||||
@Json(name = "m.asset") val locationAsset: LocationAsset? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
||||
fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
|
||||
|
||||
fun getBestTs() = ts ?: unstableTs
|
||||
|
||||
fun getBestText() = text ?: unstableText
|
||||
|
||||
fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
|
||||
|
||||
fun getBestGeoUri() = getBestLocationInfo()?.geoUri ?: geoUri
|
||||
|
||||
/**
|
||||
* @return true if the location asset is a user location, not a generic one.
|
||||
*/
|
||||
fun isSelfLocation(): Boolean {
|
||||
// Should behave like m.self if locationAsset is null
|
||||
val locationAsset = getBestLocationAsset()
|
||||
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,5 +31,9 @@ data class MessagePollContent(
|
|||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
) : MessageContent
|
||||
@Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
|
||||
@Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo
|
||||
}
|
||||
|
|
|
@ -31,5 +31,9 @@ data class MessagePollResponseContent(
|
|||
@Json(name = "body") override val body: String = "",
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
|
||||
) : MessageContent
|
||||
@Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
|
||||
@Json(name = "m.response") val response: PollResponse? = null
|
||||
) : MessageContent {
|
||||
|
||||
fun getBestResponse() = response ?: unstableResponse
|
||||
}
|
||||
|
|
|
@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class PollAnswer(
|
||||
@Json(name = "id") val id: String? = null,
|
||||
@Json(name = "org.matrix.msc1767.text") val answer: String? = null
|
||||
)
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null,
|
||||
@Json(name = "m.text") val answer: String? = null
|
||||
) {
|
||||
|
||||
fun getBestAnswer() = answer ?: unstableAnswer
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollCreationInfo(
|
||||
@Json(name = "question") val question: PollQuestion? = null,
|
||||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
@Json(name = "question") val question: PollQuestion? = null,
|
||||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
)
|
||||
|
|
|
@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PollQuestion(
|
||||
@Json(name = "org.matrix.msc1767.text") val question: String? = null
|
||||
)
|
||||
@Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null,
|
||||
@Json(name = "m.text") val question: String? = null
|
||||
) {
|
||||
|
||||
fun getBestQuestion() = question ?: unstableQuestion
|
||||
}
|
||||
|
|
|
@ -25,11 +25,17 @@ enum class PollType {
|
|||
* Voters should see results as soon as they have voted.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.disclosed")
|
||||
DISCLOSED_UNSTABLE,
|
||||
|
||||
@Json(name = "m.poll.disclosed")
|
||||
DISCLOSED,
|
||||
|
||||
/**
|
||||
* Results should be only revealed when the poll is ended.
|
||||
*/
|
||||
@Json(name = "org.matrix.msc3381.poll.undisclosed")
|
||||
UNDISCLOSED_UNSTABLE,
|
||||
|
||||
@Json(name = "m.poll.undisclosed")
|
||||
UNDISCLOSED
|
||||
}
|
||||
|
|
|
@ -26,5 +26,6 @@ data class ReactionInfo(
|
|||
@Json(name = "key") val key: String,
|
||||
// always null for reaction
|
||||
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
|
||||
@Json(name = "option") override val option: Int? = null
|
||||
@Json(name = "option") override val option: Int? = null,
|
||||
@Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
|
||||
) : RelationContent
|
||||
|
|
|
@ -24,4 +24,10 @@ interface RelationContent {
|
|||
val eventId: String?
|
||||
val inReplyTo: ReplyToContent?
|
||||
val option: Int?
|
||||
|
||||
/**
|
||||
* This flag indicates that the message should be rendered as a reply
|
||||
* fallback, when isFallingBack = false
|
||||
*/
|
||||
val isFallingBack: Boolean?
|
||||
}
|
||||
|
|
|
@ -23,5 +23,8 @@ data class RelationDefaultContent(
|
|||
@Json(name = "rel_type") override val type: String?,
|
||||
@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
|
||||
@Json(name = "option") override val option: Int? = null,
|
||||
@Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
|
||||
) : RelationContent
|
||||
|
||||
fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
|
||||
|
|
|
@ -163,13 +163,4 @@ interface RelationService {
|
|||
autoMarkdown: Boolean = false,
|
||||
formattedText: String? = null,
|
||||
eventReplied: TimelineEvent? = null): Cancelable?
|
||||
|
||||
/**
|
||||
* Get all the thread replies for the specified rootThreadEventId
|
||||
* The return list will contain the original root thread event and all the thread replies to that event
|
||||
* Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
|
||||
* from the backend
|
||||
* @param rootThreadEventId the root thread eventId
|
||||
*/
|
||||
suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
|
||||
}
|
||||
|
|
|
@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ReplyToContent(
|
||||
@Json(name = "event_id") val eventId: String? = null,
|
||||
@Json(name = "render_in") val renderIn: List<String>? = null
|
||||
@Json(name = "event_id") val eventId: String? = null
|
||||
)
|
||||
|
||||
fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true
|
||||
|
|
|
@ -32,7 +32,6 @@ object RoomSummaryConstants {
|
|||
EventType.CALL_ANSWER,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.STICKER,
|
||||
EventType.REACTION,
|
||||
EventType.POLL_START
|
||||
)
|
||||
EventType.REACTION
|
||||
) + EventType.POLL_START
|
||||
}
|
||||
|
|
|
@ -17,51 +17,43 @@
|
|||
package org.matrix.android.sdk.api.session.room.threads
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact with threads related features.
|
||||
* It's implemented at the room level within the main timeline.
|
||||
* This interface defines methods to interact with thread related features.
|
||||
* It's the dynamic threads implementation and the homeserver must return
|
||||
* a capability entry for threads. If the server do not support m.thread
|
||||
* then [ThreadsLocalService] should be used instead
|
||||
*/
|
||||
interface ThreadsService {
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
|
||||
* Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level
|
||||
*/
|
||||
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
|
||||
fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the thread root TimelineEvents that exists at the room level
|
||||
* Returns a list of all the [ThreadSummary] that exists at the room level
|
||||
*/
|
||||
fun getAllThreads(): List<TimelineEvent>
|
||||
fun getAllThreadSummaries(): List<ThreadSummary>
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotifications(): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Returns whether or not the current user is participating in the thread
|
||||
* @param rootThreadEventId the eventId of the current thread
|
||||
*/
|
||||
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Enhance the provided root thread TimelineEvent [List] by adding the latest
|
||||
* Enhance the provided ThreadSummary[List] by adding the latest
|
||||
* message edition for that thread
|
||||
* @return the enhanced [List] with edited updates
|
||||
*/
|
||||
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
|
||||
fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
|
||||
|
||||
/**
|
||||
* Marks the current thread as read in local DB.
|
||||
* note: read receipts within threads are not yet supported with the API
|
||||
* @param rootThreadEventId the root eventId of the current thread
|
||||
* Fetch all thread replies for the specified thread using the /relations api
|
||||
* @param rootThreadEventId the root thread eventId
|
||||
* @param from defines the token that will fetch from that position
|
||||
* @param limit defines the number of max results the api will respond with
|
||||
*/
|
||||
suspend fun markThreadAsRead(rootThreadEventId: String)
|
||||
suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
|
||||
|
||||
/**
|
||||
* Fetch all thread summaries for the current room using the enhanced /messages api
|
||||
*/
|
||||
suspend fun fetchThreadSummaries()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2022 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.threads.local
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact with thread related features.
|
||||
* It's the local threads implementation and assumes that the homeserver
|
||||
* do not support threads
|
||||
*/
|
||||
interface ThreadsLocalService {
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
|
||||
*/
|
||||
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the thread root TimelineEvents that exists at the room level
|
||||
*/
|
||||
fun getAllThreads(): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Returns a [LiveData] list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a list of all the marked unread threads that exists at the room level
|
||||
*/
|
||||
fun getMarkedThreadNotifications(): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Returns whether or not the current user is participating in the thread
|
||||
* @param rootThreadEventId the eventId of the current thread
|
||||
*/
|
||||
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Enhance the provided root thread TimelineEvent [List] by adding the latest
|
||||
* message edition for that thread
|
||||
* @return the enhanced [List] with edited updates
|
||||
*/
|
||||
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
|
||||
|
||||
/**
|
||||
* Marks the current thread as read in local DB.
|
||||
* note: read receipts within threads are not yet supported with the API
|
||||
* @param rootThreadEventId the root eventId of the current thread
|
||||
*/
|
||||
suspend fun markThreadAsRead(rootThreadEventId: String)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2022 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.threads.model
|
||||
|
||||
data class ThreadEditions(var rootThreadEdition: String? = null,
|
||||
var latestThreadEdition: String? = null)
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2022 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.threads.model
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
|
||||
/**
|
||||
* The main thread Summary model, mainly used to display the thread list
|
||||
*/
|
||||
data class ThreadSummary(val roomId: String,
|
||||
val rootEvent: Event?,
|
||||
val latestEvent: Event?,
|
||||
val rootEventId: String,
|
||||
val rootThreadSenderInfo: SenderInfo,
|
||||
val latestThreadSenderInfo: SenderInfo,
|
||||
val isUserParticipating: Boolean,
|
||||
val numberOfThreads: Int,
|
||||
val threadEditions: ThreadEditions = ThreadEditions())
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2022 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.threads.model
|
||||
|
||||
enum class ThreadSummaryUpdateType {
|
||||
REPLACE,
|
||||
ADD
|
||||
}
|
|
@ -54,6 +54,7 @@ data class TimelineEvent(
|
|||
* It's not unique on the timeline as it's reset on each chunk.
|
||||
*/
|
||||
val displayIndex: Int,
|
||||
var ownedByThreadChunk: Boolean = false,
|
||||
val senderInfo: SenderInfo,
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
|
@ -134,9 +135,9 @@ fun TimelineEvent.getEditedEventId(): String? {
|
|||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
return when (root.getClearType()) {
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
|
||||
in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
|
||||
else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.threads
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
|
||||
/**
|
||||
|
@ -26,7 +27,7 @@ data class ThreadDetails(
|
|||
val isRootThread: Boolean = false,
|
||||
val numberOfThreads: Int = 0,
|
||||
val threadSummarySenderInfo: SenderInfo? = null,
|
||||
val threadSummaryLatestTextMessage: String? = null,
|
||||
val threadSummaryLatestEvent: Event? = null,
|
||||
val lastMessageTimestamp: Long? = null,
|
||||
var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
|
||||
val isThread: Boolean = false,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.api.util
|
||||
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.group.model.GroupSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
@ -199,6 +200,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
|
|||
|
||||
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
|
||||
|
||||
fun SenderInfo.toMatrixItemOrNull() = tryOrNull { MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
|
||||
|
||||
fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
|
||||
MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
|
||||
} else {
|
||||
|
|
|
@ -38,7 +38,7 @@ internal data class HomeServerVersion(
|
|||
}
|
||||
|
||||
companion object {
|
||||
internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
|
||||
internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""")
|
||||
|
||||
internal fun parse(value: String): HomeServerVersion? {
|
||||
val result = pattern.matchEntire(value) ?: return null
|
||||
|
@ -56,5 +56,6 @@ internal data class HomeServerVersion(
|
|||
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
|
||||
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
|
||||
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
|
||||
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
|
|||
private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
|
||||
private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
|
||||
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
|
||||
private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
|
||||
private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
|
||||
|
||||
/**
|
||||
* Return true if the SDK supports this homeserver version
|
||||
|
@ -68,6 +70,14 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
|
|||
doesServerSeparatesAddAndBind()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate if the homeserver support MSC3440 for threads
|
||||
*/
|
||||
internal fun Versions.doesServerSupportThreads(): Boolean {
|
||||
return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
|
||||
unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the server support the lazy loading of room members
|
||||
*
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022
|
|||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
|
||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
|
||||
import org.matrix.android.sdk.internal.util.Normalizer
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
override fun equals(other: Any?) = other is RealmSessionStoreMigration
|
||||
override fun hashCode() = 1000
|
||||
|
||||
val schemaVersion = 25L
|
||||
val schemaVersion = 26L
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
|
||||
|
@ -87,5 +88,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
if (oldVersion < 23) MigrateSessionTo023(realm).perform()
|
||||
if (oldVersion < 24) MigrateSessionTo024(realm).perform()
|
||||
if (oldVersion < 25) MigrateSessionTo025(realm).perform()
|
||||
if (oldVersion < 26) MigrateSessionTo026(realm).perform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,17 +82,18 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
|
|||
internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
||||
eventEntity: EventEntity,
|
||||
direction: PaginationDirection,
|
||||
roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
|
||||
ownedByThreadChunk: Boolean = false,
|
||||
roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null): TimelineEventEntity? {
|
||||
val eventId = eventEntity.eventId
|
||||
if (timelineEvents.find(eventId) != null) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
val displayIndex = nextDisplayIndex(direction)
|
||||
val localId = TimelineEventEntity.nextId(realm)
|
||||
val senderId = eventEntity.sender ?: ""
|
||||
|
||||
// Update RR for the sender of a new message with a dummy one
|
||||
val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
|
||||
val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null
|
||||
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
|
||||
this.localId = localId
|
||||
this.root = eventEntity
|
||||
|
@ -102,6 +103,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||
?.also { it.cleanUp(eventEntity.sender) }
|
||||
this.readReceipts = readReceiptsSummaryEntity
|
||||
this.displayIndex = displayIndex
|
||||
this.ownedByThreadChunk = ownedByThreadChunk
|
||||
val roomMemberContent = roomMemberContentsByUser?.get(senderId)
|
||||
this.senderAvatar = roomMemberContent?.avatarUrl
|
||||
this.senderName = roomMemberContent?.displayName
|
||||
|
@ -113,9 +115,10 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
|||
}
|
||||
// numberOfTimelineEvents++
|
||||
timelineEvents.add(timelineEventEntity)
|
||||
return timelineEventEntity
|
||||
}
|
||||
|
||||
private fun computeIsUnique(
|
||||
fun computeIsUnique(
|
||||
realm: Realm,
|
||||
roomId: String,
|
||||
isLastForward: Boolean,
|
||||
|
|
|
@ -18,9 +18,16 @@ package org.matrix.android.sdk.internal.database.helper
|
|||
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
|
||||
internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) {
|
||||
if (!chunks.contains(chunkEntity)) {
|
||||
chunks.add(chunkEntity)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addIfNecessary(threadSummary: ThreadSummaryEntity) {
|
||||
if (!threadSummaries.contains(threadSummary)) {
|
||||
threadSummaries.add(threadSummary)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
|||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.database.query.whereRoomId
|
||||
|
||||
private typealias ThreadSummary = Pair<Int, TimelineEventEntity>?
|
||||
private typealias Summary = Pair<Int, TimelineEventEntity>?
|
||||
|
||||
/**
|
||||
* Finds the root thread event and update it with the latest message summary along with the number
|
||||
|
@ -93,11 +93,12 @@ internal fun EventEntity.markEventAsRoot(
|
|||
* @param rootThreadEventId The root eventId that will find the number of threads
|
||||
* @return A ThreadSummary containing the counted threads and the latest event message
|
||||
*/
|
||||
internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary {
|
||||
internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
|
||||
// Number of messages
|
||||
val messages = TimelineEventEntity
|
||||
.whereRoomId(realm, roomId = roomId)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||
.distinct(TimelineEventEntityFields.ROOT.EVENT_ID)
|
||||
.count()
|
||||
.toInt()
|
||||
|
||||
|
@ -123,7 +124,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
|
|||
|
||||
result ?: return null
|
||||
|
||||
return ThreadSummary(messages, result)
|
||||
return Summary(messages, result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -156,6 +157,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
|
|||
TimelineEventEntity
|
||||
.whereRoomId(realm, roomId = roomId)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
|
||||
.equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
|
||||
.sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
* Copyright 2022 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.internal.database.helper
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.createObject
|
||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||
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.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
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.CurrentStateEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
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.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||
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
|
||||
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
internal fun ThreadSummaryEntity.updateThreadSummary(
|
||||
rootThreadEventEntity: EventEntity,
|
||||
numberOfThreads: Int?,
|
||||
latestThreadEventEntity: EventEntity?,
|
||||
isUserParticipating: Boolean,
|
||||
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
|
||||
updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser)
|
||||
updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser)
|
||||
this.isUserParticipating = isUserParticipating
|
||||
numberOfThreads?.let {
|
||||
// Update number of threads only when there is an actual value
|
||||
this.numberOfThreads = it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the root thread event properties
|
||||
*/
|
||||
internal fun ThreadSummaryEntity.updateThreadSummaryRootEvent(
|
||||
rootThreadEventEntity: EventEntity,
|
||||
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
|
||||
) {
|
||||
val roomId = rootThreadEventEntity.roomId
|
||||
val rootThreadRoomMemberContent = roomMemberContentsByUser[rootThreadEventEntity.sender ?: ""]
|
||||
this.rootThreadEventEntity = rootThreadEventEntity
|
||||
this.rootThreadSenderAvatar = rootThreadRoomMemberContent?.avatarUrl
|
||||
this.rootThreadSenderName = rootThreadRoomMemberContent?.displayName
|
||||
this.rootThreadIsUniqueDisplayName = if (rootThreadRoomMemberContent?.displayName != null) {
|
||||
computeIsUnique(realm, roomId, false, rootThreadRoomMemberContent, roomMemberContentsByUser)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the latest thread event properties
|
||||
*/
|
||||
internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
|
||||
latestThreadEventEntity: EventEntity?,
|
||||
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
|
||||
) {
|
||||
val roomId = latestThreadEventEntity?.roomId ?: return
|
||||
val latestThreadRoomMemberContent = roomMemberContentsByUser[latestThreadEventEntity.sender ?: ""]
|
||||
this.latestThreadEventEntity = latestThreadEventEntity
|
||||
this.latestThreadSenderAvatar = latestThreadRoomMemberContent?.avatarUrl
|
||||
this.latestThreadSenderName = latestThreadRoomMemberContent?.displayName
|
||||
this.latestThreadIsUniqueDisplayName = if (latestThreadRoomMemberContent?.displayName != null) {
|
||||
computeIsUnique(realm, roomId, false, latestThreadRoomMemberContent, roomMemberContentsByUser)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity {
|
||||
val roomId = roomId
|
||||
val eventId = eventId
|
||||
val localId = TimelineEventEntity.nextId(realm)
|
||||
val senderId = sender ?: ""
|
||||
|
||||
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
|
||||
this.localId = localId
|
||||
this.root = this@toTimelineEventEntity
|
||||
this.eventId = eventId
|
||||
this.roomId = roomId
|
||||
this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
|
||||
?.also { it.cleanUp(sender) }
|
||||
this.ownedByThreadChunk = true // To skip it from the original event flow
|
||||
val roomMemberContent = roomMemberContentsByUser[senderId]
|
||||
this.senderAvatar = roomMemberContent?.avatarUrl
|
||||
this.senderName = roomMemberContent?.displayName
|
||||
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
|
||||
computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
return timelineEventEntity
|
||||
}
|
||||
|
||||
internal suspend fun ThreadSummaryEntity.Companion.createOrUpdate(
|
||||
threadSummaryType: ThreadSummaryUpdateType,
|
||||
realm: Realm,
|
||||
roomId: String,
|
||||
threadEventEntity: EventEntity? = null,
|
||||
rootThreadEvent: Event? = null,
|
||||
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
|
||||
roomEntity: RoomEntity,
|
||||
userId: String,
|
||||
cryptoService: CryptoService? = null
|
||||
) {
|
||||
when (threadSummaryType) {
|
||||
ThreadSummaryUpdateType.REPLACE -> {
|
||||
rootThreadEvent?.eventId ?: return
|
||||
rootThreadEvent.senderId ?: return
|
||||
|
||||
val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return
|
||||
|
||||
// Something is wrong with the server return
|
||||
if (numberOfThreads <= 0) return
|
||||
|
||||
val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
|
||||
Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
|
||||
}
|
||||
|
||||
val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also {
|
||||
decryptIfNeeded(cryptoService, it, roomId)
|
||||
}
|
||||
val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also {
|
||||
decryptIfNeeded(cryptoService, it, roomId)
|
||||
}
|
||||
val isUserParticipating = rootThreadEvent.unsignedData.relations.latestThread.isUserParticipating == true || rootThreadEvent.senderId == userId
|
||||
roomMemberContentsByUser.addSenderState(realm, roomId, rootThreadEvent.senderId)
|
||||
threadSummary.updateThreadSummary(
|
||||
rootThreadEventEntity = rootThreadEventEntity,
|
||||
numberOfThreads = numberOfThreads,
|
||||
latestThreadEventEntity = latestThreadEventEntity,
|
||||
isUserParticipating = isUserParticipating,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser
|
||||
)
|
||||
|
||||
roomEntity.addIfNecessary(threadSummary)
|
||||
}
|
||||
ThreadSummaryUpdateType.ADD -> {
|
||||
val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return
|
||||
Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
|
||||
|
||||
val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
|
||||
if (threadSummary != null) {
|
||||
// ThreadSummary exists so lets add the latest event
|
||||
Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.")
|
||||
threadSummary.updateThreadSummaryLatestEvent(threadEventEntity, roomMemberContentsByUser)
|
||||
threadSummary.numberOfThreads++
|
||||
if (threadEventEntity.sender == userId) {
|
||||
threadSummary.isUserParticipating = true
|
||||
}
|
||||
} else {
|
||||
// ThreadSummary do not exists lets try to create one
|
||||
Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
|
||||
threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
|
||||
// Root thread event entity exists so lets create a new record
|
||||
ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let {
|
||||
it.updateThreadSummary(
|
||||
rootThreadEventEntity = rootThreadEventEntity,
|
||||
numberOfThreads = 1,
|
||||
latestThreadEventEntity = threadEventEntity,
|
||||
isUserParticipating = threadEventEntity.sender == userId,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser
|
||||
)
|
||||
roomEntity.addIfNecessary(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) {
|
||||
cryptoService ?: return
|
||||
val event = eventEntity.asDomain()
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
|
||||
try {
|
||||
Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
|
||||
// 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
|
||||
)
|
||||
// Save decryption result, to not decrypt every time we enter the thread list
|
||||
eventEntity.setDecryptionResult(result)
|
||||
} catch (e: MXCryptoError) {
|
||||
if (e is MXCryptoError.Base) {
|
||||
event.mCryptoError = e.errorType
|
||||
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request decryption
|
||||
*/
|
||||
private fun requestDecryption(eventDecryptor: TimelineEventDecryptor?, event: Event?) {
|
||||
eventDecryptor ?: return
|
||||
event ?: return
|
||||
if (event.isEncrypted() &&
|
||||
event.mxDecryptionResult == null && event.eventId != null) {
|
||||
Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
|
||||
|
||||
eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(event, UUID.randomUUID().toString()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we don't have any new state on this user, get it from db
|
||||
*/
|
||||
private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
|
||||
getOrPut(senderId) {
|
||||
CurrentStateEventEntity
|
||||
.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
|
||||
?.root?.asDomain()
|
||||
?.getFixedRoomMemberContent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an EventEntity for the root thread event or get an existing one
|
||||
*/
|
||||
private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
|
||||
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||
return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an EventEntity for the latest thread event or get an existing one. Also update the user room member
|
||||
* state
|
||||
*/
|
||||
private fun createLatestEventEntity(
|
||||
roomId: String,
|
||||
rootThreadEvent: Event,
|
||||
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
|
||||
realm: Realm): EventEntity? {
|
||||
return getLatestEvent(rootThreadEvent)?.let {
|
||||
it.senderId?.let { senderId ->
|
||||
roomMemberContentsByUser.addSenderState(realm, roomId, senderId)
|
||||
}
|
||||
createEventEntity(roomId, it, realm)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returned the latest event message, if any
|
||||
*/
|
||||
private fun getLatestEvent(rootThreadEvent: Event): Event? {
|
||||
return rootThreadEvent.unsignedData?.relations?.latestThread?.event
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all ThreadSummaryEntity for the specified roomId, sorted by origin server
|
||||
* note: Sorting cannot be provided by server, so we have to use that unstable property
|
||||
* @param roomId The id of the room
|
||||
*/
|
||||
internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> =
|
||||
ThreadSummaryEntity
|
||||
.where(realm, roomId = roomId)
|
||||
.sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING)
|
||||
|
||||
/**
|
||||
* Enhance each [ThreadSummary] root and latest event with the equivalent decrypted text edition/replacement
|
||||
*/
|
||||
internal fun List<ThreadSummary>.enhanceWithEditions(realm: Realm, roomId: String): List<ThreadSummary> =
|
||||
this.map {
|
||||
it.addEditionIfNeeded(realm, roomId, true)
|
||||
it.addEditionIfNeeded(realm, roomId, false)
|
||||
it
|
||||
}
|
||||
|
||||
private fun ThreadSummary.addEditionIfNeeded(realm: Realm, roomId: String, enhanceRoot: Boolean) {
|
||||
val eventId = if (enhanceRoot) rootEventId else latestEvent?.eventId ?: return
|
||||
EventAnnotationsSummaryEntity
|
||||
.where(realm, roomId, eventId)
|
||||
.findFirst()
|
||||
?.editSummary
|
||||
?.editions
|
||||
?.lastOrNull()
|
||||
?.eventId
|
||||
?.let { editedEventId ->
|
||||
TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
|
||||
if (enhanceRoot) {
|
||||
threadEditions.rootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
|
||||
} else {
|
||||
threadEditions.latestThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -114,7 +114,7 @@ internal object EventMapper {
|
|||
)
|
||||
},
|
||||
threadNotificationState = eventEntity.threadNotificationState,
|
||||
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
|
||||
threadSummaryLatestEvent = eventEntity.threadSummaryLatestMessage?.root?.asDomain(),
|
||||
lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
|
||||
|
||||
)
|
||||
|
|
|
@ -41,7 +41,8 @@ internal object HomeServerCapabilitiesMapper {
|
|||
maxUploadFileSize = entity.maxUploadFileSize,
|
||||
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
|
||||
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
|
||||
roomVersions = mapRoomVersion(entity.roomVersionsJson)
|
||||
roomVersions = mapRoomVersion(entity.roomVersionsJson),
|
||||
canUseThreading = entity.canUseThreading
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2022 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.internal.database.mapper
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ThreadSummaryMapper @Inject constructor() {
|
||||
|
||||
fun map(threadSummary: ThreadSummaryEntity): ThreadSummary {
|
||||
return ThreadSummary(
|
||||
roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(),
|
||||
rootEvent = threadSummary.rootThreadEventEntity?.asDomain(),
|
||||
latestEvent = threadSummary.latestThreadEventEntity?.asDomain(),
|
||||
rootEventId = threadSummary.rootThreadEventId.orEmpty(),
|
||||
rootThreadSenderInfo = SenderInfo(
|
||||
userId = threadSummary.rootThreadEventEntity?.sender ?: "",
|
||||
displayName = threadSummary.rootThreadSenderName,
|
||||
isUniqueDisplayName = threadSummary.rootThreadIsUniqueDisplayName,
|
||||
avatarUrl = threadSummary.rootThreadSenderAvatar
|
||||
),
|
||||
latestThreadSenderInfo = SenderInfo(
|
||||
userId = threadSummary.latestThreadEventEntity?.sender ?: "",
|
||||
displayName = threadSummary.latestThreadSenderName,
|
||||
isUniqueDisplayName = threadSummary.latestThreadIsUniqueDisplayName,
|
||||
avatarUrl = threadSummary.latestThreadSenderAvatar
|
||||
),
|
||||
isUserParticipating = threadSummary.isUserParticipating,
|
||||
numberOfThreads = threadSummary.numberOfThreads
|
||||
)
|
||||
}
|
||||
}
|
|
@ -46,6 +46,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
|||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
avatarUrl = timelineEventEntity.senderAvatar
|
||||
),
|
||||
ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk,
|
||||
readReceipts = readReceipts
|
||||
?.distinctBy {
|
||||
it.roomMember
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.internal.database.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.FieldAttribute
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.RoomEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
|
||||
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||
|
||||
/**
|
||||
* Migrating to:
|
||||
* Live thread list: using enhanced /messages api MSC3440
|
||||
* Live thread timeline: using /relations api
|
||||
*/
|
||||
class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
|
||||
|
||||
override fun doMigrate(realm: DynamicRealm) {
|
||||
realm.schema.get("ChunkEntity")
|
||||
?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||
?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
||||
|
||||
realm.schema.get("TimelineEventEntity")
|
||||
?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
|
||||
|
||||
val eventEntity = realm.schema.get("EventEntity") ?: return
|
||||
val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
|
||||
.addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||
.addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
|
||||
.addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
|
||||
.addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
|
||||
.addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
|
||||
.addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
|
||||
.addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
|
||||
.addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
|
||||
.addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
|
||||
.addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
|
||||
.addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
|
||||
|
||||
realm.schema.get("RoomEntity")
|
||||
?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
|
||||
|
||||
realm.schema.get("HomeServerCapabilitiesEntity")
|
||||
?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
|
||||
?.forceRefreshOfHomeServerCapabilities()
|
||||
}
|
||||
}
|
|
@ -33,7 +33,10 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
|
|||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
// Only one chunk will have isLastForward == true
|
||||
@Index var isLastForward: Boolean = false,
|
||||
@Index var isLastBackward: Boolean = false
|
||||
@Index var isLastBackward: Boolean = false,
|
||||
// Threads
|
||||
@Index var rootThreadEventId: String? = null,
|
||||
@Index var isLastForwardThread: Boolean = false,
|
||||
) : RealmObject() {
|
||||
|
||||
fun identifier() = "${prevToken}_$nextToken"
|
||||
|
@ -47,14 +50,32 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
|
|||
companion object
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
|
||||
internal fun ChunkEntity.deleteOnCascade(
|
||||
deleteStateEvents: Boolean,
|
||||
canDeleteRoot: Boolean) {
|
||||
assertIsManaged()
|
||||
if (deleteStateEvents) {
|
||||
stateEvents.deleteAllFromRealm()
|
||||
}
|
||||
timelineEvents.clearWith {
|
||||
val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
|
||||
if (deleteRoot) {
|
||||
room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId)
|
||||
}
|
||||
it.deleteOnCascade(deleteRoot)
|
||||
}
|
||||
deleteFromRealm()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the chunk along with the thread events that were temporarily created
|
||||
*/
|
||||
internal fun ChunkEntity.deleteAndClearThreadEvents() {
|
||||
assertIsManaged()
|
||||
timelineEvents
|
||||
.filter { it.ownedByThreadChunk }
|
||||
.forEach {
|
||||
it.deleteOnCascade(false)
|
||||
}
|
||||
deleteFromRealm()
|
||||
}
|
||||
|
|
|
@ -34,14 +34,14 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
@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,
|
||||
var redacts: String? = null,
|
||||
var decryptionResultJson: String? = null,
|
||||
var ageLocalTs: Long? = null,
|
||||
// Thread related, no need to create a new Entity for performance
|
||||
// Thread related, no need to create a new Entity for performance
|
||||
@Index var isRootThread: Boolean = false,
|
||||
@Index var rootThreadEventId: String? = null,
|
||||
var numberOfThreads: Int = 0,
|
||||
|
|
|
@ -28,7 +28,8 @@ internal open class HomeServerCapabilitiesEntity(
|
|||
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
|
||||
var lastVersionIdentityServerSupported: Boolean = false,
|
||||
var defaultIdentityServerUrl: String? = null,
|
||||
var lastUpdatedTimestamp: Long = 0L
|
||||
var lastUpdatedTimestamp: Long = 0L,
|
||||
var canUseThreading: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
|
|
@ -20,10 +20,14 @@ import io.realm.RealmList
|
|||
import io.realm.RealmObject
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.findRootOrLatest
|
||||
import org.matrix.android.sdk.internal.extensions.assertIsManaged
|
||||
|
||||
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
|
||||
var chunks: RealmList<ChunkEntity> = RealmList(),
|
||||
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
var threadSummaries: RealmList<ThreadSummaryEntity> = RealmList(),
|
||||
var accountData: RealmList<RoomAccountDataEntity> = RealmList()
|
||||
) : RealmObject() {
|
||||
|
||||
|
@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
|
|||
}
|
||||
companion object
|
||||
}
|
||||
internal fun RoomEntity.removeThreadSummaryIfNeeded(eventId: String) {
|
||||
assertIsManaged()
|
||||
threadSummaries.findRootOrLatest(eventId)?.let {
|
||||
threadSummaries.remove(it)
|
||||
it.deleteFromRealm()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model
|
|||
|
||||
import io.realm.annotations.RealmModule
|
||||
import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
|
||||
/**
|
||||
* Realm module for Session
|
||||
|
@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit
|
|||
RoomAccountDataEntity::class,
|
||||
SpaceChildSummaryEntity::class,
|
||||
SpaceParentSummaryEntity::class,
|
||||
UserPresenceEntity::class
|
||||
UserPresenceEntity::class,
|
||||
ThreadSummaryEntity::class
|
||||
])
|
||||
internal class SessionRealmModule
|
||||
|
|
|
@ -32,6 +32,9 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
|||
var isUniqueDisplayName: Boolean = false,
|
||||
var senderAvatar: String? = null,
|
||||
var senderMembershipEventId: String? = null,
|
||||
// ownedByThreadChunk indicates that the current TimelineEventEntity belongs
|
||||
// to a thread chunk and is a temporarily event.
|
||||
var ownedByThreadChunk: Boolean = false,
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null
|
||||
) : RealmObject() {
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2022 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.internal.database.model.threads
|
||||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||
|
||||
internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String? = "",
|
||||
var rootThreadEventEntity: EventEntity? = null,
|
||||
var latestThreadEventEntity: EventEntity? = null,
|
||||
var rootThreadSenderName: String? = null,
|
||||
var latestThreadSenderName: String? = null,
|
||||
var rootThreadSenderAvatar: String? = null,
|
||||
var latestThreadSenderAvatar: String? = null,
|
||||
var rootThreadIsUniqueDisplayName: Boolean = false,
|
||||
var isUserParticipating: Boolean = false,
|
||||
var latestThreadIsUniqueDisplayName: Boolean = false,
|
||||
var numberOfThreads: Int = 0
|
||||
) : RealmObject() {
|
||||
|
||||
@LinkingObjects("threadSummaries")
|
||||
val room: RealmResults<RoomEntity>? = null
|
||||
|
||||
companion object
|
||||
}
|
|
@ -45,10 +45,22 @@ internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, room
|
|||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.Companion.findLastForwardChunkOfThread(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? {
|
||||
return where(realm, roomId)
|
||||
.equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
|
||||
.findFirst()
|
||||
}
|
||||
internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: String, event: String): ChunkEntity? {
|
||||
return where(realm, roomId)
|
||||
.`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, arrayListOf(event).toTypedArray())
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
|
||||
.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()
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId
|
|||
this.roomId = roomId
|
||||
}
|
||||
// Denormalization
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findAll()?.forEach {
|
||||
it.annotations = obj
|
||||
}
|
||||
return obj
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2022 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.internal.database.query
|
||||
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.createObject
|
||||
import io.realm.kotlin.where
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
|
||||
|
||||
internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> {
|
||||
return realm.where<ThreadSummaryEntity>()
|
||||
.equalTo(ThreadSummaryEntityFields.ROOM.ROOM_ID, roomId)
|
||||
}
|
||||
|
||||
internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String, rootThreadEventId: String): RealmQuery<ThreadSummaryEntity> {
|
||||
return where(realm, roomId)
|
||||
.equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||
}
|
||||
|
||||
internal fun ThreadSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity {
|
||||
return where(realm, roomId, rootThreadEventId).findFirst() ?: realm.createObject<ThreadSummaryEntity>().apply {
|
||||
this.rootThreadEventId = rootThreadEventId
|
||||
}
|
||||
}
|
||||
internal fun ThreadSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity? {
|
||||
return where(realm, roomId, rootThreadEventId).findFirst()
|
||||
}
|
||||
internal fun RealmList<ThreadSummaryEntity>.find(rootThreadEventId: String): ThreadSummaryEntity? {
|
||||
return this.where()
|
||||
.equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
internal fun RealmList<ThreadSummaryEntity>.findRootOrLatest(eventId: String): ThreadSummaryEntity? {
|
||||
return this.where()
|
||||
.beginGroup()
|
||||
.equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, eventId)
|
||||
.or()
|
||||
.equalTo(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.EVENT_ID, eventId)
|
||||
.endGroup()
|
||||
.findFirst()
|
||||
}
|
|
@ -17,9 +17,21 @@
|
|||
package org.matrix.android.sdk.internal.session.filter
|
||||
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
import timber.log.Timber
|
||||
|
||||
internal object FilterFactory {
|
||||
|
||||
fun createThreadsFilter(numberOfEvents: Int, userId: String?): RoomEventFilter {
|
||||
Timber.i("$userId")
|
||||
return RoomEventFilter(
|
||||
limit = numberOfEvents,
|
||||
// senders = listOf(userId),
|
||||
// relationSenders = userId?.let { listOf(it) },
|
||||
relationTypes = listOf(RelationType.THREAD)
|
||||
)
|
||||
}
|
||||
|
||||
fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
|
||||
return RoomEventFilter(
|
||||
limit = numberOfEvents,
|
||||
|
@ -58,8 +70,8 @@ internal object FilterFactory {
|
|||
|
||||
private fun createElementTimelineFilter(): RoomEventFilter? {
|
||||
return null // RoomEventFilter().apply {
|
||||
// TODO Enable this for optimization
|
||||
// types = listOfSupportedEventTypes.toMutableList()
|
||||
// TODO Enable this for optimization
|
||||
// types = listOfSupportedEventTypes.toMutableList()
|
||||
// }
|
||||
}
|
||||
|
||||
|
|
|
@ -52,12 +52,13 @@ data class RoomEventFilter(
|
|||
* A list of relation types which must be exist pointing to the event being filtered.
|
||||
* If this list is absent then no filtering is done on relation types.
|
||||
*/
|
||||
@Json(name = "relation_types") val relationTypes: List<String>? = null,
|
||||
@Json(name = "related_by_rel_types") val relationTypes: List<String>? = null,
|
||||
/**
|
||||
* A list of senders of relations which must exist pointing to the event being filtered.
|
||||
* If this list is absent then no filtering is done on relation types.
|
||||
*/
|
||||
@Json(name = "relation_senders") val relationSenders: List<String>? = null,
|
||||
@Json(name = "related_by_senders") val relationSenders: List<String>? = null,
|
||||
|
||||
/**
|
||||
* A list of room IDs to include. If this list is absent then all rooms are included.
|
||||
*/
|
||||
|
|
|
@ -65,7 +65,13 @@ internal data class Capabilities(
|
|||
* Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
|
||||
*/
|
||||
@Json(name = "m.room_versions")
|
||||
val roomVersions: RoomVersions? = null
|
||||
val roomVersions: RoomVersions? = null,
|
||||
/**
|
||||
* Capability to indicate if the server supports MSC3440 Threading
|
||||
* True if the user can use m.thread relation, false otherwise
|
||||
*/
|
||||
@Json(name = "m.thread")
|
||||
val threads: BooleanCapability? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
@ -20,9 +20,11 @@ import com.zhuinden.monarchy.Monarchy
|
|||
import org.matrix.android.sdk.api.MatrixPatterns.getDomain
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.orTrue
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
import org.matrix.android.sdk.internal.auth.version.Versions
|
||||
import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
|
||||
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
|
||||
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
|
@ -121,6 +123,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
|||
homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
|
||||
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
|
||||
}
|
||||
homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
|
||||
getVersionResult?.doesServerSupportThreads().orFalse()
|
||||
}
|
||||
|
||||
if (getMediaConfigResult != null) {
|
||||
|
|
|
@ -56,7 +56,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
|||
|
||||
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
||||
when (event.type) {
|
||||
EventType.POLL_START,
|
||||
in EventType.POLL_START,
|
||||
EventType.MESSAGE,
|
||||
EventType.REDACTION,
|
||||
EventType.ENCRYPTED,
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
|
|||
import org.matrix.android.sdk.api.session.room.state.StateService
|
||||
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
||||
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
|
||||
import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||
import org.matrix.android.sdk.api.session.room.uploads.UploadsService
|
||||
|
@ -56,6 +57,7 @@ internal class DefaultRoom(override val roomId: String,
|
|||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||
private val timelineService: TimelineService,
|
||||
private val threadsService: ThreadsService,
|
||||
private val threadsLocalService: ThreadsLocalService,
|
||||
private val sendService: SendService,
|
||||
private val draftService: DraftService,
|
||||
private val stateService: StateService,
|
||||
|
@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String,
|
|||
Room,
|
||||
TimelineService by timelineService,
|
||||
ThreadsService by threadsService,
|
||||
ThreadsLocalService by threadsLocalService,
|
||||
SendService by sendService,
|
||||
DraftService by draftService,
|
||||
StateService by stateService,
|
||||
|
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.PagedList
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.room.Room
|
||||
import org.matrix.android.sdk.api.session.room.RoomService
|
||||
|
@ -109,6 +110,10 @@ internal class DefaultRoomService @Inject constructor(
|
|||
return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
|
||||
}
|
||||
|
||||
override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> {
|
||||
return roomSummaryDataSource.getCountFlow(queryParams)
|
||||
}
|
||||
|
||||
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
||||
return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryE
|
|||
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
|
||||
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.create
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
|
@ -86,11 +87,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.POLL_START,
|
||||
EventType.POLL_RESPONSE,
|
||||
EventType.POLL_END
|
||||
)
|
||||
EventType.ENCRYPTED
|
||||
) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END
|
||||
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
return allowedTypes.contains(eventType)
|
||||
|
@ -117,8 +115,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
|
||||
EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
|
||||
?.let {
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst()
|
||||
?.let { tet -> tet.annotations = it }
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
|
||||
?.forEach { tet -> tet.annotations = it }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,7 +154,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
} else if (event.getClearType() == EventType.POLL_RESPONSE) {
|
||||
} else if (event.getClearType() in EventType.POLL_RESPONSE) {
|
||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
|
@ -177,12 +175,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
handleVerification(realm, event, roomId, isLocalEcho, it)
|
||||
}
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
in EventType.POLL_RESPONSE -> {
|
||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
|
||||
}
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
in EventType.POLL_END -> {
|
||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
|
@ -196,6 +194,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
handleReaction(realm, event, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
// HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
|
||||
// else if (event.unsignedData?.relations?.annotations != null) {
|
||||
// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
|
||||
// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
|
||||
// EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
|
||||
// ?.let {
|
||||
// TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
|
||||
// ?.forEach { tet -> tet.annotations = it }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
EventType.REDACTION -> {
|
||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||
|
@ -217,7 +225,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
EventType.POLL_START -> {
|
||||
in EventType.POLL_START -> {
|
||||
val content: MessagePollContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
|
@ -225,12 +233,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
EventType.POLL_RESPONSE -> {
|
||||
in EventType.POLL_RESPONSE -> {
|
||||
event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
EventType.POLL_END -> {
|
||||
in EventType.POLL_END -> {
|
||||
event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
|
||||
handleEndPoll(realm, event, it, roomId, isLocalEcho)
|
||||
}
|
||||
|
@ -243,7 +251,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
|
||||
// OPT OUT serer aggregation until API mature enough
|
||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
|
||||
|
||||
private fun handleReplace(realm: Realm,
|
||||
event: Event,
|
||||
|
@ -335,13 +343,18 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
}
|
||||
|
||||
if (!isLocalEcho) {
|
||||
val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
|
||||
val replaceEvent = TimelineEventEntity
|
||||
.where(realm, roomId, eventId)
|
||||
.equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
|
||||
.findFirst()
|
||||
handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the edition is on the latest thread event, and update it accordingly
|
||||
* @param editedEvent The event that will be changed
|
||||
* @param replaceEvent The new event
|
||||
*/
|
||||
private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
|
||||
replaceEvent: TimelineEventEntity?,
|
||||
|
@ -407,12 +420,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
val option = content.response?.answers?.first() ?: return Unit.also {
|
||||
val option = content.getBestResponse()?.answers?.first() ?: return Unit.also {
|
||||
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
|
||||
// Check if this option is in available options
|
||||
if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) {
|
||||
if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) {
|
||||
Timber.v("## POLL $targetEventId doesn't contain option $option")
|
||||
return
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room
|
|||
|
||||
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.RelationType
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
|
||||
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||
|
@ -86,7 +87,7 @@ internal interface RoomAPI {
|
|||
suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String,
|
||||
@Query("from") from: String,
|
||||
@Query("dir") dir: String,
|
||||
@Query("limit") limit: Int,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("filter") filter: String?
|
||||
): PaginationResponse
|
||||
|
||||
|
@ -218,7 +219,6 @@ internal interface RoomAPI {
|
|||
|
||||
/**
|
||||
* Paginate relations for event based in normal topological order
|
||||
*
|
||||
* @param relationType filter for this relation type
|
||||
* @param eventType filter for this event type
|
||||
*/
|
||||
|
@ -227,9 +227,24 @@ internal interface RoomAPI {
|
|||
@Path("eventId") eventId: String,
|
||||
@Path("relationType") relationType: String,
|
||||
@Path("eventType") eventType: String,
|
||||
@Query("from") from: String? = null,
|
||||
@Query("to") to: String? = null,
|
||||
@Query("limit") limit: Int? = null
|
||||
): RelationsResponse
|
||||
|
||||
/**
|
||||
* Paginate relations for thread events based in normal topological order
|
||||
* @param relationType filter for this relation type
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
|
||||
suspend fun getThreadsRelations(@Path("roomId") roomId: String,
|
||||
@Path("eventId") eventId: String,
|
||||
@Path("relationType") relationType: String = RelationType.THREAD,
|
||||
@Query("from") from: String? = null,
|
||||
@Query("to") to: String? = null,
|
||||
@Query("limit") limit: Int? = null
|
||||
): RelationsResponse
|
||||
|
||||
/**
|
||||
* Join the given room.
|
||||
*
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask
|
|||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
|
||||
import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
|
||||
import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
|
||||
import org.matrix.android.sdk.internal.session.room.threads.local.DefaultThreadsLocalService
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
|
||||
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
|
||||
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
|
||||
|
@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
|||
private val roomSummaryDataSource: RoomSummaryDataSource,
|
||||
private val timelineServiceFactory: DefaultTimelineService.Factory,
|
||||
private val threadsServiceFactory: DefaultThreadsService.Factory,
|
||||
private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory,
|
||||
private val sendServiceFactory: DefaultSendService.Factory,
|
||||
private val draftServiceFactory: DefaultDraftService.Factory,
|
||||
private val stateServiceFactory: DefaultStateService.Factory,
|
||||
|
@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
|||
roomSummaryDataSource = roomSummaryDataSource,
|
||||
timelineService = timelineServiceFactory.create(roomId),
|
||||
threadsService = threadsServiceFactory.create(roomId),
|
||||
threadsLocalService = threadsLocalServiceFactory.create(roomId),
|
||||
sendService = sendServiceFactory.create(roomId),
|
||||
draftService = draftServiceFactory.create(roomId),
|
||||
stateService = stateServiceFactory.create(roomId),
|
||||
|
|
|
@ -77,7 +77,9 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
|
|||
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
|
||||
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
|
||||
|
@ -294,4 +296,7 @@ internal abstract class RoomModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
|
|||
when (typeToPrune) {
|
||||
EventType.ENCRYPTED,
|
||||
EventType.MESSAGE,
|
||||
EventType.POLL_START -> {
|
||||
in EventType.POLL_START -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||
?: UnsignedData(null, null)
|
||||
|
|
|
@ -34,7 +34,6 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain
|
|||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||
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.timeline.TimelineEventDataSource
|
||||
|
@ -48,7 +47,6 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
private val eventFactory: LocalEchoEventFactory,
|
||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
|
||||
private val timelineEventDataSource: TimelineEventDataSource,
|
||||
@SessionDatabase private val monarchy: Monarchy
|
||||
) : RelationService {
|
||||
|
@ -196,10 +194,6 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
return eventSenderProcessor.postEvent(event)
|
||||
}
|
||||
|
||||
override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
|
||||
return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the event in database as a local echo.
|
||||
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright 2022 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.internal.session.room.relation.threads
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
|
||||
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||
import org.matrix.android.sdk.internal.database.helper.createOrUpdate
|
||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.filter.FilterFactory
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
/***
|
||||
* This class is responsible to Fetch all the thread in the current room,
|
||||
* To fetch all threads in a room, the /messages API is used with newly added filtering options.
|
||||
*/
|
||||
internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, DefaultFetchThreadSummariesTask.Result> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val from: String = "",
|
||||
val limit: Int = 100,
|
||||
val isUserParticipating: Boolean = true
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultFetchThreadSummariesTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val cryptoService: DefaultCryptoService,
|
||||
@UserId private val userId: String,
|
||||
) : FetchThreadSummariesTask {
|
||||
|
||||
override suspend fun execute(params: FetchThreadSummariesTask.Params): Result {
|
||||
val filter = FilterFactory.createThreadsFilter(
|
||||
numberOfEvents = params.limit,
|
||||
userId = if (params.isUserParticipating) userId else null).toJSONString()
|
||||
|
||||
val response = executeRequest(
|
||||
globalErrorReceiver,
|
||||
canRetry = true
|
||||
) {
|
||||
roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
|
||||
}
|
||||
|
||||
Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ")
|
||||
|
||||
return handleResponse(response, params)
|
||||
}
|
||||
|
||||
private suspend fun handleResponse(response: PaginationResponse,
|
||||
params: FetchThreadSummariesTask.Params): Result {
|
||||
val rootThreadList = response.events
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
|
||||
|
||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||
for (rootThreadEvent in rootThreadList) {
|
||||
if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
ThreadSummaryEntity.createOrUpdate(
|
||||
threadSummaryType = ThreadSummaryUpdateType.REPLACE,
|
||||
realm = realm,
|
||||
roomId = params.roomId,
|
||||
rootThreadEvent = rootThreadEvent,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser,
|
||||
roomEntity = roomEntity,
|
||||
userId = userId,
|
||||
cryptoService = cryptoService)
|
||||
}
|
||||
}
|
||||
return Result.SUCCESS
|
||||
}
|
||||
|
||||
enum class Result {
|
||||
SHOULD_FETCH_MORE,
|
||||
REACHED_END,
|
||||
SUCCESS
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
* Copyright 2022 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.
|
||||
|
@ -20,14 +20,12 @@ import io.realm.Realm
|
|||
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.RelationType
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
|
||||
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
|
||||
|
@ -36,8 +34,10 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
|
|||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
||||
import org.matrix.android.sdk.internal.database.query.find
|
||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
|
||||
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
|
||||
|
@ -47,16 +47,38 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
|||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
|
||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
|
||||
/***
|
||||
* This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API
|
||||
*
|
||||
* How it works
|
||||
*
|
||||
* The problem?
|
||||
* - We cannot use the existing timeline architecture to paginate through the timeline
|
||||
* - We want our new events to be live, so any interactions with them like reactions will continue to work. We should
|
||||
* handle appropriately the existing events from /messages api with the new events from /relations.
|
||||
* - Handling edge cases like receiving an event from /messages while you have already created a new one from the /relations response
|
||||
*
|
||||
* The solution
|
||||
* We generate a temporarily thread chunk that will be used to store any new paginated results from the /relations api
|
||||
* We bind the timeline events from that chunk with the already existing ones. So we will have one common instance, and
|
||||
* all reactions, edits etc will continue to work. If the events do not exists we create them
|
||||
* and we will reuse the same EventEntity instance when (and if) the same event will be fetched from the main (/messages) timeline
|
||||
*
|
||||
*/
|
||||
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, DefaultFetchThreadTimelineTask.Result> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val rootThreadEventId: String
|
||||
val rootThreadEventId: String,
|
||||
val from: String?,
|
||||
val limit: Int
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -69,93 +91,129 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
private val cryptoService: DefaultCryptoService
|
||||
) : FetchThreadTimelineTask {
|
||||
|
||||
override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
|
||||
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
|
||||
enum class Result {
|
||||
SHOULD_FETCH_MORE,
|
||||
REACHED_END,
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
|
||||
val response = executeRequest(globalErrorReceiver) {
|
||||
roomAPI.getRelations(
|
||||
roomAPI.getThreadsRelations(
|
||||
roomId = params.roomId,
|
||||
eventId = params.rootThreadEventId,
|
||||
relationType = RelationType.IO_THREAD,
|
||||
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
|
||||
limit = 2000
|
||||
from = params.from,
|
||||
limit = params.limit
|
||||
)
|
||||
}
|
||||
|
||||
val threadList = response.chunks + listOfNotNull(response.originalEvent)
|
||||
Timber.i("###THREADS FetchThreadTimelineTask Fetched size:${response.chunks.size} nextBatch:${response.nextBatch} ")
|
||||
return handleRelationsResponse(response, params)
|
||||
}
|
||||
|
||||
return storeNewEventsIfNeeded(threadList, params.roomId)
|
||||
private suspend fun handleRelationsResponse(response: RelationsResponse,
|
||||
params: FetchThreadTimelineTask.Params): Result {
|
||||
val threadList = response.chunks
|
||||
val threadRootEvent = response.originalEvent
|
||||
val hasReachEnd = response.nextBatch == null
|
||||
|
||||
monarchy.awaitTransaction { realm ->
|
||||
|
||||
val threadChunk = ChunkEntity.findLastForwardChunkOfThread(realm, params.roomId, params.rootThreadEventId)
|
||||
?: run {
|
||||
return@awaitTransaction
|
||||
}
|
||||
|
||||
threadChunk.prevToken = response.nextBatch
|
||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||
|
||||
for (event in threadList) {
|
||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (threadChunk.timelineEvents.find(event.eventId) != null) {
|
||||
// Event already exists in thread chunk, skip it
|
||||
Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} already exists in thread chunk, skip it")
|
||||
continue
|
||||
}
|
||||
|
||||
val timelineEvent = TimelineEventEntity
|
||||
.where(realm, roomId = params.roomId, event.eventId)
|
||||
.findFirst()
|
||||
|
||||
if (timelineEvent != null) {
|
||||
// Event already exists but not in the thread chunk
|
||||
// Lets added there
|
||||
Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} exists but not in the thread chunk, add it at the end")
|
||||
threadChunk.timelineEvents.add(timelineEvent)
|
||||
} else {
|
||||
Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} is brand NEW create an entity and add it!")
|
||||
val eventEntity = createEventEntity(params.roomId, event, realm)
|
||||
roomMemberContentsByUser.addSenderState(realm, params.roomId, event.senderId)
|
||||
threadChunk.addTimelineEvent(
|
||||
roomId = params.roomId,
|
||||
eventEntity = eventEntity,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
ownedByThreadChunk = true,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReachEnd) {
|
||||
val rootThread = TimelineEventEntity
|
||||
.where(realm, roomId = params.roomId, params.rootThreadEventId)
|
||||
.findFirst()
|
||||
if (rootThread != null) {
|
||||
// If root thread event already exists add it to our chunk
|
||||
threadChunk.timelineEvents.add(rootThread)
|
||||
Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} found and added!")
|
||||
} else if (threadRootEvent?.senderId != null) {
|
||||
// Case when thread event is not in the device
|
||||
Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} NOT FOUND! Lets create a temp one")
|
||||
val eventEntity = createEventEntity(params.roomId, threadRootEvent, realm)
|
||||
roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId)
|
||||
threadChunk.addTimelineEvent(
|
||||
roomId = params.roomId,
|
||||
eventEntity = eventEntity,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
ownedByThreadChunk = true,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (hasReachEnd) {
|
||||
Result.REACHED_END
|
||||
} else {
|
||||
Result.SHOULD_FETCH_MORE
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Reuse this function to all the app
|
||||
/**
|
||||
* If we don't have any new state on this user, get it from db
|
||||
*/
|
||||
private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
|
||||
getOrPut(senderId) {
|
||||
CurrentStateEventEntity
|
||||
.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
|
||||
?.root?.asDomain()
|
||||
?.getFixedRoomMemberContent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new events if they are not already received, and returns weather or not,
|
||||
* a timeline update should be made
|
||||
* @param threadList is the list containing the thread replies
|
||||
* @param roomId the roomId of the the thread
|
||||
* @return
|
||||
* Create an EventEntity to be added in the TimelineEventEntity
|
||||
*/
|
||||
private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
|
||||
var eventsSkipped = 0
|
||||
monarchy
|
||||
.awaitTransaction { realm ->
|
||||
val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
|
||||
|
||||
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||
|
||||
for (event in threadList.reversed()) {
|
||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
eventsSkipped++
|
||||
continue
|
||||
}
|
||||
|
||||
if (EventEntity.where(realm, event.eventId).findFirst() != null) {
|
||||
// Skip if event already exists
|
||||
eventsSkipped++
|
||||
continue
|
||||
}
|
||||
if (event.isEncrypted()) {
|
||||
// Decrypt events that will be stored
|
||||
decryptIfNeeded(event, roomId)
|
||||
}
|
||||
|
||||
handleReaction(realm, event, roomId)
|
||||
|
||||
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
|
||||
|
||||
// Sender info
|
||||
roomMemberContentsByUser.getOrPut(event.senderId) {
|
||||
// If we don't have any new state on this user, get it from db
|
||||
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
|
||||
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
||||
}
|
||||
|
||||
chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||
eventEntity.rootThreadEventId?.let {
|
||||
// This is a thread event
|
||||
optimizedThreadSummaryMap[it] = eventEntity
|
||||
} ?: run {
|
||||
// This is a normal event or a root thread one
|
||||
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
|
||||
}
|
||||
}
|
||||
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
|
||||
roomId = roomId,
|
||||
realm = realm,
|
||||
currentUserId = userId,
|
||||
shouldUpdateNotifications = false
|
||||
)
|
||||
}
|
||||
Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
|
||||
|
||||
return eventsSkipped == threadList.size
|
||||
private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
|
||||
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||
return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the event decryption mechanism for a specific event
|
||||
*/
|
||||
|
||||
private suspend fun decryptIfNeeded(event: Event, roomId: String) {
|
||||
try {
|
||||
// Event from sync does not have roomId, so add it to the event first
|
||||
|
|
|
@ -137,16 +137,11 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
options: List<String>,
|
||||
pollType: PollType): MessagePollContent {
|
||||
return MessagePollContent(
|
||||
pollCreationInfo = PollCreationInfo(
|
||||
question = PollQuestion(
|
||||
question = question
|
||||
),
|
||||
unstablePollCreationInfo = PollCreationInfo(
|
||||
question = PollQuestion(unstableQuestion = question),
|
||||
kind = pollType,
|
||||
answers = options.map { option ->
|
||||
PollAnswer(
|
||||
id = UUID.randomUUID().toString(),
|
||||
answer = option
|
||||
)
|
||||
PollAnswer(id = UUID.randomUUID().toString(), unstableAnswer = option)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -167,7 +162,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_START,
|
||||
type = EventType.POLL_START.first(),
|
||||
content = newContent.toContent()
|
||||
)
|
||||
}
|
||||
|
@ -179,11 +174,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
body = answerId,
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.REFERENCE,
|
||||
eventId = pollEventId),
|
||||
response = PollResponse(
|
||||
answers = listOf(answerId)
|
||||
)
|
||||
|
||||
eventId = pollEventId
|
||||
),
|
||||
unstableResponse = PollResponse(answers = listOf(answerId))
|
||||
)
|
||||
val localId = LocalEcho.createLocalEchoId()
|
||||
return Event(
|
||||
|
@ -191,7 +184,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_RESPONSE,
|
||||
type = EventType.POLL_RESPONSE.first(),
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
@ -207,7 +200,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_START,
|
||||
type = EventType.POLL_START.first(),
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
@ -226,7 +219,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
originServerTs = dummyOriginServerTs(),
|
||||
senderId = userId,
|
||||
eventId = localId,
|
||||
type = EventType.POLL_END,
|
||||
type = EventType.POLL_END.first(),
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
}
|
||||
|
@ -239,15 +232,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
val content = MessageLocationContent(
|
||||
geoUri = geoUri,
|
||||
body = geoUri,
|
||||
locationInfo = LocationInfo(
|
||||
geoUri = geoUri,
|
||||
description = geoUri
|
||||
),
|
||||
locationAsset = LocationAsset(
|
||||
type = LocationAssetType.SELF
|
||||
),
|
||||
ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
|
||||
text = geoUri
|
||||
unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
|
||||
unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF),
|
||||
unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
|
||||
unstableText = geoUri
|
||||
)
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
@ -353,8 +341,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
url = attachment.queryUri.toString(),
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
type = RelationType.THREAD,
|
||||
eventId = it,
|
||||
isFallingBack = true,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
|
@ -396,8 +385,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
url = attachment.queryUri.toString(),
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
type = RelationType.THREAD,
|
||||
eventId = it,
|
||||
isFallingBack = true,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
|
@ -426,8 +416,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
type = RelationType.THREAD,
|
||||
eventId = it,
|
||||
isFallingBack = true,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
|
@ -446,8 +437,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
url = attachment.queryUri.toString(),
|
||||
relatesTo = rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
type = RelationType.THREAD,
|
||||
eventId = it,
|
||||
isFallingBack = true,
|
||||
inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
|
||||
)
|
||||
}
|
||||
|
@ -479,7 +471,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
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 isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.THREAD
|
||||
val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
|
||||
if (isThread && rootThreadEventId != null) {
|
||||
val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
|
||||
|
@ -560,7 +552,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
relatesTo = generateReplyRelationContent(
|
||||
eventId = eventId,
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
showAsReply = showInThread))
|
||||
showInThread = showInThread))
|
||||
return createMessageEvent(roomId, content)
|
||||
}
|
||||
|
||||
|
@ -570,18 +562,20 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
* "m.relates_to": {
|
||||
* "rel_type": "m.thread",
|
||||
* "event_id": "$thread_root",
|
||||
* "is_falling_back": false,
|
||||
* "m.in_reply_to": {
|
||||
* "event_id": "$event_target",
|
||||
* "render_in": ["m.thread"]
|
||||
* }
|
||||
* }
|
||||
* "event_id": "$event_target"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
|
||||
private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showInThread: Boolean): RelationDefaultContent =
|
||||
rootThreadEventId?.let {
|
||||
RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
type = RelationType.THREAD,
|
||||
eventId = it,
|
||||
inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
|
||||
isFallingBack = showInThread,
|
||||
// False when is a rich reply from within a thread, and true when is a reply that should be visible from threads
|
||||
inReplyTo = ReplyToContent(eventId = eventId))
|
||||
} ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
|
||||
|
||||
private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
|
||||
|
@ -638,7 +632,9 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
|
||||
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
|
||||
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
|
||||
MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "")
|
||||
MessageType.MSGTYPE_POLL_START -> {
|
||||
return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
|
||||
}
|
||||
else -> return TextContent(content?.body ?: "")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,8 +58,9 @@ fun TextContent.toThreadTextContent(
|
|||
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
|
||||
body = text,
|
||||
relatesTo = RelationDefaultContent(
|
||||
type = RelationType.IO_THREAD,
|
||||
type = RelationType.THREAD,
|
||||
eventId = rootThreadEventId,
|
||||
isFallingBack = true,
|
||||
inReplyTo = ReplyToContent(
|
||||
eventId = latestThreadEventId
|
||||
)),
|
||||
|
|
|
@ -25,7 +25,13 @@ import androidx.paging.PagedList
|
|||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.toFlow
|
||||
import io.realm.kotlin.where
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||
import org.matrix.android.sdk.api.query.isNormalized
|
||||
|
@ -42,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
|
|||
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
|
||||
|
@ -55,8 +62,10 @@ import javax.inject.Inject
|
|||
|
||||
internal class RoomSummaryDataSource @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val realmSessionProvider: RealmSessionProvider,
|
||||
private val roomSummaryMapper: RoomSummaryMapper,
|
||||
private val queryStringValueProcessor: QueryStringValueProcessor
|
||||
private val queryStringValueProcessor: QueryStringValueProcessor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) {
|
||||
|
||||
fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
|
||||
|
@ -219,17 +228,29 @@ internal class RoomSummaryDataSource @Inject constructor(
|
|||
return object : UpdatableLivePageResult {
|
||||
override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
|
||||
|
||||
override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
|
||||
realmDataSourceFactory.updateQuery {
|
||||
roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
|
||||
}
|
||||
}
|
||||
|
||||
override val liveBoundaries: LiveData<ResultBoundaries>
|
||||
get() = boundaries
|
||||
|
||||
override var queryParams: RoomSummaryQueryParams = queryParams
|
||||
set(value) {
|
||||
field = value
|
||||
realmDataSourceFactory.updateQuery {
|
||||
roomSummariesQuery(it, value).process(sortOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCountFlow(queryParams: RoomSummaryQueryParams): Flow<Int> =
|
||||
realmSessionProvider
|
||||
.withRealm { realm -> roomSummariesQuery(realm, queryParams).findAllAsync() }
|
||||
.toFlow()
|
||||
// need to create the flow on a context dispatcher with a thread with attached Looper
|
||||
.flowOn(coroutineDispatchers.main)
|
||||
.map { it.size }
|
||||
.flowOn(coroutineDispatchers.io)
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
|
||||
var notificationCount: RoomAggregateNotificationCount? = null
|
||||
monarchy.doWithRealm { realm ->
|
||||
|
|
|
@ -23,25 +23,25 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.session.room.threads.ThreadsService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
|
||||
import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions
|
||||
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
|
||||
import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
|
||||
import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
|
||||
import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||
|
||||
internal class DefaultThreadsService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
@UserId private val userId: String,
|
||||
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
|
||||
private val fetchThreadSummariesTask: FetchThreadSummariesTask,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val threadSummaryMapper: ThreadSummaryMapper
|
||||
) : ThreadsService {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -49,55 +49,40 @@ internal class DefaultThreadsService @AssistedInject constructor(
|
|||
fun create(roomId: String): DefaultThreadsService
|
||||
}
|
||||
|
||||
override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
|
||||
override fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
{ ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||
{
|
||||
threadSummaryMapper.map(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMarkedThreadNotifications(): List<TimelineEvent> {
|
||||
override fun getAllThreadSummaries(): List<ThreadSummary> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
{ ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||
{ threadSummaryMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAllThreads(): List<TimelineEvent> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
|
||||
override fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary> {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
TimelineEventEntity.isUserParticipatingInThread(
|
||||
realm = it,
|
||||
roomId = roomId,
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
senderId = userId)
|
||||
threads.enhanceWithEditions(it, roomId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
threads.mapEventsWithEdition(it, roomId)
|
||||
}
|
||||
override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) {
|
||||
fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
|
||||
roomId = roomId,
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
from = from,
|
||||
limit = limit
|
||||
))
|
||||
}
|
||||
|
||||
override suspend fun markThreadAsRead(rootThreadEventId: String) {
|
||||
monarchy.awaitTransaction {
|
||||
EventEntity.where(
|
||||
realm = it,
|
||||
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
|
||||
}
|
||||
override suspend fun fetchThreadSummaries() {
|
||||
fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params(
|
||||
roomId = roomId
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2022 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.internal.session.room.threads.local
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.realm.Realm
|
||||
import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
|
||||
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
|
||||
import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
|
||||
import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
|
||||
internal class DefaultThreadsLocalService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
@UserId private val userId: String,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
) : ThreadsLocalService {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): DefaultThreadsLocalService
|
||||
}
|
||||
|
||||
override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMarkedThreadNotifications(): List<TimelineEvent> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAllThreads(): List<TimelineEvent> {
|
||||
return monarchy.fetchAllMappedSync(
|
||||
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
TimelineEventEntity.isUserParticipatingInThread(
|
||||
realm = it,
|
||||
roomId = roomId,
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
senderId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
threads.mapEventsWithEdition(it, roomId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun markThreadAsRead(rootThreadEventId: String) {
|
||||
monarchy.awaitTransaction {
|
||||
EventEntity.where(
|
||||
realm = it,
|
||||
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
|||
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
|
||||
import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
|
||||
|
@ -58,6 +59,7 @@ internal class DefaultTimeline(private val roomId: String,
|
|||
paginationTask: PaginationTask,
|
||||
getEventTask: GetContextOfEventTask,
|
||||
fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
fetchThreadTimelineTask: FetchThreadTimelineTask,
|
||||
timelineEventMapper: TimelineEventMapper,
|
||||
timelineInput: TimelineInput,
|
||||
threadsAwarenessHandler: ThreadsAwarenessHandler,
|
||||
|
@ -89,7 +91,9 @@ internal class DefaultTimeline(private val roomId: String,
|
|||
realm = backgroundRealm,
|
||||
eventDecryptor = eventDecryptor,
|
||||
paginationTask = paginationTask,
|
||||
realmConfiguration = realmConfiguration,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
fetchThreadTimelineTask = fetchThreadTimelineTask,
|
||||
getContextOfEventTask = getEventTask,
|
||||
timelineInput = timelineInput,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
|
@ -297,7 +301,13 @@ internal class DefaultTimeline(private val roomId: String,
|
|||
Timber.v("Post snapshot of ${snapshot.size} events")
|
||||
withContext(coroutineDispatchers.main) {
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onTimelineUpdated(snapshot) }
|
||||
if (initialEventId != null && isFromThreadTimeline && snapshot.firstOrNull { it.eventId == initialEventId } == null) {
|
||||
// We are in a thread timeline with a permalink, post update timeline only when the appropriate message have been found
|
||||
tryOrNull { it.onTimelineUpdated(arrayListOf()) }
|
||||
} else {
|
||||
// In all the other cases update timeline as expected
|
||||
tryOrNull { it.onTimelineUpdated(snapshot) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsS
|
|||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
|
||||
|
||||
|
@ -42,6 +43,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
private val eventDecryptor: TimelineEventDecryptor,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
|
||||
|
@ -64,10 +66,11 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
realmConfiguration = monarchy.realmConfiguration,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
paginationTask = paginationTask,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
timelineInput = timelineInput,
|
||||
eventDecryptor = eventDecryptor,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
fetchThreadTimelineTask = fetchThreadTimelineTask,
|
||||
loadRoomMembersTask = loadRoomMembersTask,
|
||||
readReceiptHandler = readReceiptHandler,
|
||||
getEventTask = contextOfEventTask,
|
||||
|
|
|
@ -19,20 +19,28 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
|||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import io.realm.kotlin.createObject
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.internal.database.helper.addIfNecessary
|
||||
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
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.RoomEntity
|
||||
import org.matrix.android.sdk.internal.database.model.deleteAndClearThreadEvents
|
||||
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
|
||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
|
@ -76,6 +84,8 @@ internal class LoadTimelineStrategy(
|
|||
val realm: AtomicReference<Realm>,
|
||||
val eventDecryptor: TimelineEventDecryptor,
|
||||
val paginationTask: PaginationTask,
|
||||
val realmConfiguration: RealmConfiguration,
|
||||
val fetchThreadTimelineTask: FetchThreadTimelineTask,
|
||||
val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
val getContextOfEventTask: GetContextOfEventTask,
|
||||
val timelineInput: TimelineInput,
|
||||
|
@ -90,7 +100,6 @@ internal class LoadTimelineStrategy(
|
|||
private var getContextLatch: CompletableDeferred<Unit>? = null
|
||||
private var chunkEntity: RealmResults<ChunkEntity>? = null
|
||||
private var timelineChunk: TimelineChunk? = null
|
||||
|
||||
private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults<ChunkEntity>, changeSet: OrderedCollectionChangeSet ->
|
||||
// Can be call either when you open a permalink on an unknown event
|
||||
// or when there is a gap in the timeline.
|
||||
|
@ -170,6 +179,9 @@ internal class LoadTimelineStrategy(
|
|||
getContextLatch?.cancel()
|
||||
chunkEntity = null
|
||||
timelineChunk = null
|
||||
if (mode is Mode.Thread) {
|
||||
clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
|
||||
|
@ -185,6 +197,9 @@ internal class LoadTimelineStrategy(
|
|||
return LoadMoreResult.FAILURE
|
||||
}
|
||||
}
|
||||
if (mode is Mode.Thread) {
|
||||
return timelineChunk?.loadMoreThread(count) ?: LoadMoreResult.FAILURE
|
||||
}
|
||||
return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
|
||||
}
|
||||
|
||||
|
@ -201,7 +216,7 @@ internal class LoadTimelineStrategy(
|
|||
}
|
||||
|
||||
private fun buildSendingEvents(): List<TimelineEvent> {
|
||||
return if (hasReachedLastForward()) {
|
||||
return if (hasReachedLastForward() || mode is Mode.Thread) {
|
||||
sendingEventsDataSource.buildSendingEvents()
|
||||
} else {
|
||||
emptyList()
|
||||
|
@ -219,13 +234,47 @@ internal class LoadTimelineStrategy(
|
|||
ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
|
||||
}
|
||||
is Mode.Thread -> {
|
||||
recreateThreadChunkEntity(realm, mode.rootThreadEventId)
|
||||
ChunkEntity.where(realm, roomId)
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||
.equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, mode.rootThreadEventId)
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
|
||||
.findAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any existing thread chunk entity and create a new one, with the
|
||||
* rootThreadEventId included
|
||||
*/
|
||||
private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||
realm.executeTransaction {
|
||||
// Lets delete the chunk and start a new one
|
||||
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
||||
Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
|
||||
}
|
||||
val threadChunk = it.createObject<ChunkEntity>().apply {
|
||||
Timber.i("###THREADS LoadTimelineStrategy [onStart] Created new thread chunk with rootThreadEventId: $rootThreadEventId")
|
||||
this.rootThreadEventId = rootThreadEventId
|
||||
this.isLastForwardThread = true
|
||||
}
|
||||
if (threadChunk.isValid) {
|
||||
RoomEntity.where(it, roomId).findFirst()?.addIfNecessary(threadChunk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any existing thread chunk
|
||||
*/
|
||||
private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
|
||||
realm.executeTransaction {
|
||||
ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
|
||||
Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasReachedLastForward(): Boolean {
|
||||
return timelineChunk?.hasReachedLastForward().orFalse()
|
||||
}
|
||||
|
@ -237,8 +286,10 @@ internal class LoadTimelineStrategy(
|
|||
timelineSettings = dependencies.timelineSettings,
|
||||
roomId = roomId,
|
||||
timelineId = timelineId,
|
||||
fetchThreadTimelineTask = dependencies.fetchThreadTimelineTask,
|
||||
eventDecryptor = dependencies.eventDecryptor,
|
||||
paginationTask = dependencies.paginationTask,
|
||||
realmConfiguration = dependencies.realmConfiguration,
|
||||
fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
|
||||
timelineEventMapper = dependencies.timelineEventMapper,
|
||||
uiEchoManager = uiEchoManager,
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
|||
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmObjectChangeListener
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
|
@ -36,6 +37,8 @@ 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.session.room.relation.threads.DefaultFetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
|
||||
import timber.log.Timber
|
||||
import java.util.Collections
|
||||
|
@ -50,8 +53,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
private val timelineSettings: TimelineSettings,
|
||||
private val roomId: String,
|
||||
private val timelineId: String,
|
||||
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
|
||||
private val eventDecryptor: TimelineEventDecryptor,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val uiEchoManager: UIEchoManager? = null,
|
||||
|
@ -141,6 +146,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
val loadFromStorage = loadFromStorage(count, direction).also {
|
||||
logLoadedFromStorage(it, direction)
|
||||
}
|
||||
if (loadFromStorage.numberOfEvents == 6) {
|
||||
Timber.i("here")
|
||||
}
|
||||
|
||||
val offsetCount = count - loadFromStorage.numberOfEvents
|
||||
|
||||
|
@ -157,6 +165,29 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will fetch more live thread timeline events using the /relations api. It will
|
||||
* always fetch results, while we want our data to be up to dated.
|
||||
*/
|
||||
suspend fun loadMoreThread(count: Int, direction: Timeline.Direction = Timeline.Direction.BACKWARDS): LoadMoreResult {
|
||||
val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE
|
||||
return if (direction == Timeline.Direction.BACKWARDS) {
|
||||
try {
|
||||
fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
|
||||
roomId,
|
||||
rootThreadEventId,
|
||||
chunkEntity.prevToken,
|
||||
count
|
||||
)).toLoadMoreResult()
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Failed to fetch thread timeline events from the server")
|
||||
LoadMoreResult.FAILURE
|
||||
}
|
||||
} else {
|
||||
LoadMoreResult.FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult {
|
||||
return if (direction == Timeline.Direction.FORWARDS) {
|
||||
val nextChunkEntity = chunkEntity.nextChunk
|
||||
|
@ -413,6 +444,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
}
|
||||
}
|
||||
|
||||
private fun DefaultFetchThreadTimelineTask.Result.toLoadMoreResult(): LoadMoreResult {
|
||||
return when (this) {
|
||||
DefaultFetchThreadTimelineTask.Result.REACHED_END -> LoadMoreResult.REACHED_END
|
||||
DefaultFetchThreadTimelineTask.Result.SHOULD_FETCH_MORE,
|
||||
DefaultFetchThreadTimelineTask.Result.SUCCESS -> LoadMoreResult.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOffsetIndex(): Int {
|
||||
var offset = 0
|
||||
var currentNextChunk = nextChunk
|
||||
|
@ -454,6 +493,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
|
||||
onBuiltEvents(true)
|
||||
}
|
||||
|
@ -487,6 +527,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
|
|||
timelineId = timelineId,
|
||||
eventDecryptor = eventDecryptor,
|
||||
paginationTask = paginationTask,
|
||||
realmConfiguration = realmConfiguration,
|
||||
fetchThreadTimelineTask = fetchThreadTimelineTask,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
uiEchoManager = uiEchoManager,
|
||||
|
@ -538,7 +580,6 @@ private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmR
|
|||
.or()
|
||||
.equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
|
||||
.endGroup()
|
||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
.findAll()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.EventEntity
|
|||
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.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||
import org.matrix.android.sdk.internal.database.query.create
|
||||
import org.matrix.android.sdk.internal.database.query.find
|
||||
|
@ -49,10 +50,10 @@ import javax.inject.Inject
|
|||
* Insert Chunk in DB, and eventually link next and previous chunk in db.
|
||||
*/
|
||||
internal class TokenChunkEventPersistor @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
@UserId private val userId: String,
|
||||
private val lightweightSettingsStorage: LightweightSettingsStorage,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>) {
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
@UserId private val userId: String,
|
||||
private val lightweightSettingsStorage: LightweightSettingsStorage,
|
||||
private val liveEventManager: Lazy<StreamEventsManager>) {
|
||||
|
||||
enum class Result {
|
||||
SHOULD_FETCH_MORE,
|
||||
|
@ -145,9 +146,12 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
if (event.eventId == null || event.senderId == null) {
|
||||
return@forEach
|
||||
}
|
||||
// We check for the timeline event with this id
|
||||
// We check for the timeline event with this id, but not in the thread chunk
|
||||
val eventId = event.eventId
|
||||
val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
|
||||
val existingTimelineEvent = TimelineEventEntity
|
||||
.where(realm, roomId, eventId)
|
||||
.equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
|
||||
.findFirst()
|
||||
// If it exists, we want to stop here, just link the prevChunk
|
||||
val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
|
||||
if (existingChunk != null) {
|
||||
|
@ -173,7 +177,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
return@processTimelineEvents
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it }
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
var eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
|
||||
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
|
||||
event.prevContent
|
||||
|
@ -183,7 +187,11 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
|
||||
}
|
||||
liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
|
||||
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||
currentChunk.addTimelineEvent(
|
||||
roomId = roomId,
|
||||
eventEntity = eventEntity,
|
||||
direction = direction,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
eventEntity.rootThreadEventId?.let {
|
||||
// This is a thread event
|
||||
|
|
|
@ -24,10 +24,12 @@ 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.toModel
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
|
||||
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.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
|
||||
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
|
||||
|
@ -37,6 +39,7 @@ 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.helper.createOrUpdate
|
||||
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
|
||||
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
|
@ -47,10 +50,13 @@ import org.matrix.android.sdk.internal.database.model.EventEntity
|
|||
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.model.threads.ThreadSummaryEntity
|
||||
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.findLastForwardChunkOfRoom
|
||||
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
|
||||
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
|
||||
|
@ -85,6 +91,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
|
||||
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
|
||||
@UserId private val userId: String,
|
||||
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
|
||||
private val lightweightSettingsStorage: LightweightSettingsStorage,
|
||||
private val timelineInput: TimelineInput,
|
||||
private val liveEventService: Lazy<StreamEventsManager>) {
|
||||
|
@ -95,11 +102,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||
}
|
||||
|
||||
fun handle(realm: Realm,
|
||||
roomsSyncResponse: RoomsSyncResponse,
|
||||
isInitialSync: Boolean,
|
||||
aggregator: SyncResponsePostTreatmentAggregator,
|
||||
reporter: ProgressReporter? = null) {
|
||||
suspend fun handle(realm: Realm,
|
||||
roomsSyncResponse: RoomsSyncResponse,
|
||||
isInitialSync: Boolean,
|
||||
aggregator: SyncResponsePostTreatmentAggregator,
|
||||
reporter: ProgressReporter? = null) {
|
||||
Timber.v("Execute transaction from $this")
|
||||
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
|
||||
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
|
||||
|
@ -114,11 +121,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
}
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun handleRoomSync(realm: Realm,
|
||||
handlingStrategy: HandlingStrategy,
|
||||
isInitialSync: Boolean,
|
||||
aggregator: SyncResponsePostTreatmentAggregator,
|
||||
reporter: ProgressReporter?) {
|
||||
private suspend fun handleRoomSync(realm: Realm,
|
||||
handlingStrategy: HandlingStrategy,
|
||||
isInitialSync: Boolean,
|
||||
aggregator: SyncResponsePostTreatmentAggregator,
|
||||
reporter: ProgressReporter?) {
|
||||
val insertType = if (isInitialSync) {
|
||||
EventInsertType.INITIAL_SYNC
|
||||
} else {
|
||||
|
@ -151,11 +158,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
realm.insertOrUpdate(rooms)
|
||||
}
|
||||
|
||||
private fun insertJoinRoomsFromInitSync(realm: Realm,
|
||||
handlingStrategy: HandlingStrategy.JOINED,
|
||||
syncLocalTimeStampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator,
|
||||
reporter: ProgressReporter?) {
|
||||
private suspend fun insertJoinRoomsFromInitSync(realm: Realm,
|
||||
handlingStrategy: HandlingStrategy.JOINED,
|
||||
syncLocalTimeStampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator,
|
||||
reporter: ProgressReporter?) {
|
||||
val bestChunkSize = computeBestChunkSize(
|
||||
listSize = handlingStrategy.data.keys.size,
|
||||
limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
|
||||
|
@ -193,12 +200,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleJoinedRoom(realm: Realm,
|
||||
roomId: String,
|
||||
roomSync: RoomSync,
|
||||
insertType: EventInsertType,
|
||||
syncLocalTimestampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
|
||||
private suspend fun handleJoinedRoom(realm: Realm,
|
||||
roomId: String,
|
||||
roomSync: RoomSync,
|
||||
insertType: EventInsertType,
|
||||
syncLocalTimestampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
|
||||
Timber.v("Handle join sync for room $roomId")
|
||||
|
||||
val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
|
||||
|
@ -344,15 +351,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
return roomEntity
|
||||
}
|
||||
|
||||
private fun handleTimelineEvents(realm: Realm,
|
||||
roomId: String,
|
||||
roomEntity: RoomEntity,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true,
|
||||
insertType: EventInsertType,
|
||||
syncLocalTimestampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
|
||||
private suspend fun handleTimelineEvents(realm: Realm,
|
||||
roomId: String,
|
||||
roomEntity: RoomEntity,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true,
|
||||
insertType: EventInsertType,
|
||||
syncLocalTimestampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
|
||||
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
||||
if (isLimited && lastChunk != null) {
|
||||
lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
|
||||
|
@ -409,11 +416,28 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
||||
}
|
||||
|
||||
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||
val timelineEventAdded = chunkEntity.addTimelineEvent(
|
||||
roomId = roomId,
|
||||
eventEntity = eventEntity,
|
||||
direction = PaginationDirection.FORWARDS,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
|
||||
eventEntity.rootThreadEventId?.let {
|
||||
// This is a thread event
|
||||
optimizedThreadSummaryMap[it] = eventEntity
|
||||
// Add the same thread timeline event to Thread Chunk
|
||||
addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
|
||||
if (homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
|
||||
// Update thread summaries only if homeserver supports threading
|
||||
ThreadSummaryEntity.createOrUpdate(
|
||||
threadSummaryType = ThreadSummaryUpdateType.ADD,
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
threadEventEntity = eventEntity,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser,
|
||||
userId = userId,
|
||||
roomEntity = roomEntity)
|
||||
}
|
||||
} ?: run {
|
||||
// This is a normal event or a root thread one
|
||||
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
|
||||
|
@ -458,6 +482,28 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
return chunkEntity
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new event to the appropriate thread chunk. If the event is already in
|
||||
* the thread timeline and /relations api, we should not added it
|
||||
*/
|
||||
private fun addToThreadChunkIfNeeded(realm: Realm,
|
||||
roomId: String,
|
||||
threadId: String,
|
||||
timelineEventEntity: TimelineEventEntity?,
|
||||
roomEntity: RoomEntity) {
|
||||
val eventId = timelineEventEntity?.eventId ?: return
|
||||
|
||||
ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk ->
|
||||
val existingEvent = threadChunk.timelineEvents.find(eventId)
|
||||
if (existingEvent?.ownedByThreadChunk == true) {
|
||||
Timber.i("###THREADS RoomSyncHandler event:${timelineEventEntity.eventId} already exists, do not add")
|
||||
return@addToThreadChunkIfNeeded
|
||||
}
|
||||
threadChunk.timelineEvents.add(0, timelineEventEntity)
|
||||
roomEntity.addIfNecessary(threadChunk)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun decryptIfNeeded(event: Event, roomId: String) {
|
||||
try {
|
||||
// Event from sync does not have roomId, so add it to the event first
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||
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.RelationDefaultContent
|
||||
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
|
||||
|
@ -161,7 +162,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
eventEntity: EventEntity? = null): String? {
|
||||
event ?: return null
|
||||
roomId ?: return null
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
|
||||
if (lightweightSettingsStorage.areThreadMessagesEnabled() && !isReplyEvent(event)) return null
|
||||
handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
|
||||
if (!isThreadEvent(event)) return null
|
||||
val eventPayload = if (!event.isEncrypted()) {
|
||||
|
@ -170,8 +171,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
|
||||
}
|
||||
val eventBody = event.getDecryptedTextSummary() ?: return null
|
||||
val threadRelation = getRootThreadRelationContent(event)
|
||||
val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
|
||||
return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
|
||||
return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
|
||||
}
|
||||
val eventToInject = getEventFromDB(realm, eventIdToInject)
|
||||
val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
|
||||
|
@ -183,17 +185,19 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
roomId = roomId,
|
||||
eventBody = eventBody,
|
||||
eventToInject = eventToInject,
|
||||
eventToInjectBody = eventToInjectBody) ?: return null
|
||||
eventToInjectBody = eventToInjectBody,
|
||||
threadRelation = threadRelation) ?: return null
|
||||
|
||||
// update the event
|
||||
contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
|
||||
} else {
|
||||
contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
|
||||
contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
|
||||
}
|
||||
|
||||
// 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)
|
||||
handleEventsThatRelatesTo(realm, roomId, event, eventBody, false, threadRelation)
|
||||
}
|
||||
return contentForNonEncrypted
|
||||
}
|
||||
|
@ -205,11 +209,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
* @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? {
|
||||
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)
|
||||
return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true, null)
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
@ -224,7 +233,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
* @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? {
|
||||
private fun handleEventsThatRelatesTo(
|
||||
realm: Realm,
|
||||
roomId: String,
|
||||
event: Event,
|
||||
eventBody: String,
|
||||
isFromCache: Boolean,
|
||||
threadRelation: RelationDefaultContent?
|
||||
): String? {
|
||||
event.eventId ?: return null
|
||||
val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
|
||||
eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
|
||||
|
@ -236,7 +252,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
roomId = roomId,
|
||||
eventBody = newEventBody,
|
||||
eventToInject = event,
|
||||
eventToInjectBody = eventBody) ?: return null
|
||||
eventToInjectBody = eventBody,
|
||||
threadRelation = threadRelation) ?: return null
|
||||
|
||||
return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
|
||||
}
|
||||
|
@ -280,7 +297,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
private fun injectEvent(roomId: String,
|
||||
eventBody: String,
|
||||
eventToInject: Event,
|
||||
eventToInjectBody: String): Content? {
|
||||
eventToInjectBody: String,
|
||||
threadRelation: RelationDefaultContent?
|
||||
): Content? {
|
||||
val eventToInjectId = eventToInject.eventId ?: return null
|
||||
val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
|
||||
val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
|
||||
|
@ -293,6 +312,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
eventBody)
|
||||
|
||||
return MessageTextContent(
|
||||
relatesTo = threadRelation,
|
||||
msgType = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageFormat.FORMAT_MATRIX_HTML,
|
||||
body = eventBody,
|
||||
|
@ -306,12 +326,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
private fun injectFallbackIndicator(event: Event,
|
||||
eventBody: String,
|
||||
eventEntity: EventEntity?,
|
||||
eventPayload: MutableMap<String, Any>): String? {
|
||||
eventPayload: MutableMap<String, Any>,
|
||||
threadRelation: RelationDefaultContent?): String? {
|
||||
val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
|
||||
"In reply to a thread",
|
||||
eventBody)
|
||||
|
||||
val messageTextContent = MessageTextContent(
|
||||
relatesTo = threadRelation,
|
||||
msgType = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageFormat.FORMAT_MATRIX_HTML,
|
||||
body = eventBody,
|
||||
|
@ -332,7 +354,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
.findAll()
|
||||
cacheEventRootId.add(rootThreadEventId)
|
||||
return threadList.filter {
|
||||
it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
|
||||
it.asDomain().getRelationContentForType(RelationType.THREAD)?.inReplyTo?.eventId == currentEventId
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -350,7 +372,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
* @param event
|
||||
*/
|
||||
private fun isThreadEvent(event: Event): Boolean =
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.IO_THREAD
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.THREAD
|
||||
|
||||
/**
|
||||
* Returns the root thread eventId or null otherwise
|
||||
|
@ -359,9 +381,22 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
private fun getRootThreadEventId(event: Event): String? =
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
|
||||
private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? =
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo
|
||||
|
||||
private fun getPreviousEventOrRoot(event: Event): String? =
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId
|
||||
|
||||
/**
|
||||
* Returns if we should html inject the current event.
|
||||
*/
|
||||
private fun isReplyEvent(event: Event): Boolean {
|
||||
return isThreadEvent(event) && !isFallingBack(event) && getPreviousEventOrRoot(event) != null
|
||||
}
|
||||
|
||||
private fun isFallingBack(event: Event): Boolean =
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.isFallingBack == true
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
|
||||
val content = payload?.get("content") as? JsonDict
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data
|
|||
import org.amshove.kluent.shouldBe
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.internal.auth.version.Versions
|
||||
import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
|
||||
import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
|
||||
|
||||
class VersionsKtTest {
|
||||
|
@ -53,5 +54,20 @@ class VersionsKtTest {
|
|||
Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true
|
||||
Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true
|
||||
Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true
|
||||
Versions(supportedVersions = listOf("v1.6.0")).isSupportedBySdk() shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doesServerSupportThreads() {
|
||||
Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false
|
||||
Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false
|
||||
Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false
|
||||
Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true
|
||||
Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true
|
||||
Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true
|
||||
Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
|
||||
Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
|
||||
Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false
|
||||
Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -355,6 +355,7 @@ dependencies {
|
|||
// Lifecycle
|
||||
implementation libs.androidx.lifecycleLivedata
|
||||
implementation libs.androidx.lifecycleProcess
|
||||
implementation libs.androidx.lifecycleRuntimeKtx
|
||||
|
||||
implementation libs.androidx.datastore
|
||||
implementation libs.androidx.datastorepreferences
|
||||
|
|
|
@ -27,6 +27,7 @@ import im.vector.app.espresso.tools.ScreenshotFailureRule
|
|||
import im.vector.app.features.MainActivity
|
||||
import im.vector.app.getString
|
||||
import im.vector.app.ui.robot.ElementRobot
|
||||
import im.vector.app.ui.robot.settings.labs.LabFeature
|
||||
import im.vector.app.ui.robot.withDeveloperMode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
@ -97,6 +98,8 @@ class UiAllScreensSanityTest {
|
|||
}
|
||||
}
|
||||
|
||||
testThreadScreens()
|
||||
|
||||
elementRobot.space {
|
||||
createSpace {
|
||||
crawl()
|
||||
|
@ -148,4 +151,25 @@ class UiAllScreensSanityTest {
|
|||
// TODO Deactivate account instead of logout?
|
||||
elementRobot.signout(expectSignOutWarning = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing multiple threads screens
|
||||
*/
|
||||
private fun testThreadScreens() {
|
||||
elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
|
||||
elementRobot.newRoom {
|
||||
createNewRoom {
|
||||
crawl()
|
||||
createRoom {
|
||||
val message = "Hello This message will be a thread!"
|
||||
postMessage(message)
|
||||
replyToThread(message)
|
||||
viewInRoom(message)
|
||||
openThreadSummaries()
|
||||
selectThreadSummariesFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,15 @@
|
|||
package im.vector.app.ui.robot
|
||||
|
||||
import android.view.View
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.Espresso.pressBack
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import com.adevinta.android.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||
import com.adevinta.android.barista.interaction.BaristaDialogInteractions.clickDialogNegativeButton
|
||||
|
@ -35,6 +41,7 @@ import im.vector.app.features.home.HomeActivity
|
|||
import im.vector.app.features.onboarding.OnboardingActivity
|
||||
import im.vector.app.initialSyncIdlingResource
|
||||
import im.vector.app.ui.robot.settings.SettingsRobot
|
||||
import im.vector.app.ui.robot.settings.labs.LabFeature
|
||||
import im.vector.app.ui.robot.space.SpaceRobot
|
||||
import im.vector.app.withIdlingResource
|
||||
import timber.log.Timber
|
||||
|
@ -70,11 +77,11 @@ class ElementRobot {
|
|||
}
|
||||
}
|
||||
|
||||
fun settings(block: SettingsRobot.() -> Unit) {
|
||||
fun settings(shouldGoBack: Boolean = true, block: SettingsRobot.() -> Unit) {
|
||||
openDrawer()
|
||||
clickOn(R.id.homeDrawerHeaderSettingsView)
|
||||
block(SettingsRobot())
|
||||
pressBack()
|
||||
if (shouldGoBack) pressBack()
|
||||
waitUntilViewVisible(withId(R.id.bottomNavigationView))
|
||||
}
|
||||
|
||||
|
@ -103,6 +110,22 @@ class ElementRobot {
|
|||
waitUntilViewVisible(withId(R.id.bottomNavigationView))
|
||||
}
|
||||
|
||||
fun toggleLabFeature(labFeature: LabFeature) {
|
||||
when (labFeature) {
|
||||
LabFeature.THREAD_MESSAGES -> {
|
||||
settings(shouldGoBack = false) {
|
||||
labs(shouldGoBack = false) {
|
||||
onView(withText(R.string.labs_enable_thread_messages))
|
||||
.check(ViewAssertions.matches(isDisplayed()))
|
||||
.perform(ViewActions.closeSoftKeyboard(), click())
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun signout(expectSignOutWarning: Boolean) {
|
||||
clickOn(R.id.groupToolbarAvatarImageView)
|
||||
clickOn(R.id.homeDrawerHeaderSignoutView)
|
||||
|
|
|
@ -70,4 +70,13 @@ class MessageMenuRobot(
|
|||
clickOn(R.string.edit)
|
||||
autoClosed = true
|
||||
}
|
||||
|
||||
fun replyInThread() {
|
||||
clickOn(R.string.reply_in_thread)
|
||||
autoClosed = true
|
||||
}
|
||||
fun viewInRoom() {
|
||||
clickOn(R.string.view_in_room)
|
||||
autoClosed = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,23 @@ class RoomDetailRobot {
|
|||
pressBack()
|
||||
}
|
||||
|
||||
fun replyToThread(message: String) {
|
||||
openMessageMenu(message) {
|
||||
replyInThread()
|
||||
}
|
||||
val threadMessage = "Hello universe - long message to avoid espresso tapping edited!"
|
||||
writeTo(R.id.composerEditText, threadMessage)
|
||||
waitUntilViewVisible(withId(R.id.sendButton))
|
||||
clickOn(R.id.sendButton)
|
||||
}
|
||||
|
||||
fun viewInRoom(message: String) {
|
||||
openMessageMenu(message) {
|
||||
viewInRoom()
|
||||
}
|
||||
waitUntilViewVisible(withId(R.id.composerEditText))
|
||||
}
|
||||
|
||||
fun crawlMessage(message: String) {
|
||||
// Test quick reaction
|
||||
val quickReaction = EmojiDataSource.quickEmojis[0] // 👍
|
||||
|
@ -110,7 +127,7 @@ class RoomDetailRobot {
|
|||
onView(withId(R.id.timelineRecyclerView))
|
||||
.perform(
|
||||
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
|
||||
ViewMatchers.hasDescendant(ViewMatchers.withText(message)),
|
||||
ViewMatchers.hasDescendant(withText(message)),
|
||||
ViewActions.longClick()
|
||||
)
|
||||
)
|
||||
|
@ -130,4 +147,16 @@ class RoomDetailRobot {
|
|||
block(RoomSettingsRobot())
|
||||
pressBack()
|
||||
}
|
||||
|
||||
fun openThreadSummaries() {
|
||||
clickMenu(R.id.menu_timeline_thread_list)
|
||||
waitUntilViewVisible(withId(R.id.threadListRecyclerView))
|
||||
}
|
||||
|
||||
fun selectThreadSummariesFilter() {
|
||||
clickMenu(R.id.menu_thread_list_filter)
|
||||
sleep(1000)
|
||||
clickOn(R.id.threadListModalMyThreads)
|
||||
pressBack()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.ui.robot.settings
|
||||
|
||||
import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn
|
||||
import im.vector.app.R
|
||||
import im.vector.app.clickOnAndGoBack
|
||||
|
||||
|
@ -51,8 +52,13 @@ class SettingsRobot {
|
|||
clickOnAndGoBack(R.string.settings_security_and_privacy) { block(SettingsSecurityRobot()) }
|
||||
}
|
||||
|
||||
fun labs(block: () -> Unit = {}) {
|
||||
clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() }
|
||||
fun labs(shouldGoBack: Boolean = true, block: () -> Unit = {}) {
|
||||
if (shouldGoBack) {
|
||||
clickOnAndGoBack(R.string.room_settings_labs_pref_title) { block() }
|
||||
} else {
|
||||
clickOn(R.string.room_settings_labs_pref_title)
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
fun advancedSettings(block: SettingsAdvancedRobot.() -> Unit) {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.ui.robot.settings.labs
|
||||
|
||||
enum class LabFeature {
|
||||
SWIPE_TO_REPLY,
|
||||
TAB_UNREAD_NOTIFICATIONS,
|
||||
LATEX_MATHEMATICS,
|
||||
THREAD_MESSAGES,
|
||||
AUTO_REPORT_ERRORS,
|
||||
RENDER_USER_LOCATION
|
||||
}
|
|
@ -22,13 +22,17 @@ import androidx.datastore.preferences.core.Preferences
|
|||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import im.vector.app.features.HomeserverCapabilitiesOverride
|
||||
import im.vector.app.features.VectorOverrides
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_overrides")
|
||||
private val keyForceDialPadDisplay = booleanPreferencesKey("force_dial_pad_display")
|
||||
private val keyForceLoginFallback = booleanPreferencesKey("force_login_fallback")
|
||||
private val forceCanChangeDisplayName = booleanPreferencesKey("force_can_change_display_name")
|
||||
private val forceCanChangeAvatar = booleanPreferencesKey("force_can_change_avatar")
|
||||
|
||||
class DebugVectorOverrides(private val context: Context) : VectorOverrides {
|
||||
|
||||
|
@ -40,6 +44,13 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
|
|||
preferences[keyForceLoginFallback].orFalse()
|
||||
}
|
||||
|
||||
override val forceHomeserverCapabilities = context.dataStore.data.map { preferences ->
|
||||
HomeserverCapabilitiesOverride(
|
||||
canChangeDisplayName = preferences[forceCanChangeDisplayName],
|
||||
canChangeAvatar = preferences[forceCanChangeAvatar]
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setForceDialPadDisplay(force: Boolean) {
|
||||
context.dataStore.edit { settings ->
|
||||
settings[keyForceDialPadDisplay] = force
|
||||
|
@ -51,4 +62,18 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
|
|||
settings[keyForceLoginFallback] = force
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setHomeserverCapabilities(block: HomeserverCapabilitiesOverride.() -> HomeserverCapabilitiesOverride) {
|
||||
val capabilitiesOverride = block(forceHomeserverCapabilities.firstOrNull() ?: HomeserverCapabilitiesOverride(null, null))
|
||||
context.dataStore.edit { settings ->
|
||||
when (capabilitiesOverride.canChangeDisplayName) {
|
||||
null -> settings.remove(forceCanChangeDisplayName)
|
||||
else -> settings[forceCanChangeDisplayName] = capabilitiesOverride.canChangeDisplayName
|
||||
}
|
||||
when (capabilitiesOverride.canChangeAvatar) {
|
||||
null -> settings.remove(forceCanChangeAvatar)
|
||||
else -> settings[forceCanChangeAvatar] = capabilitiesOverride.canChangeAvatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue